diff --git a/.env.example b/.env.example index 35db1dd..9f57aad 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,8 @@ -APP_NAME=Laravel +APP_NAME=Crawlshot APP_ENV=local APP_KEY= APP_DEBUG=true -APP_URL=http://localhost +APP_URL=https://crawlshot.test APP_LOCALE=en APP_FALLBACK_LOCALE=en diff --git a/API_DOCUMENTATION.md b/API_DOCUMENTATION.md new file mode 100644 index 0000000..4c27570 --- /dev/null +++ b/API_DOCUMENTATION.md @@ -0,0 +1,489 @@ +# Crawlshot API Documentation + +Crawlshot is a self-hosted web crawling and screenshot service built with Laravel and Spatie Browsershot. This API provides endpoints for capturing web content and generating screenshots with advanced filtering capabilities. + +## Base URL + +``` +https://crawlshot.test +``` + +## Authentication + +All API endpoints (except health check) require authentication using Laravel Sanctum API tokens. + +### Authentication Header + +```http +Authorization: Bearer {your-api-token} +``` + +### Example API Token +``` +1|rrWUM5ZkmLfGipkm1oIusYX45KbukIekUwMjgB3Nd1121a5c +``` + +--- + +## Health Check + +### GET `/api/health` + +Check if the Crawlshot service is running and healthy. + +**Authentication:** Not required + +#### Request Example + +```bash +curl -X GET "https://crawlshot.test/api/health" \ + -H "Accept: application/json" +``` + +#### Response Example + +```json +{ + "status": "healthy", + "timestamp": "2025-08-10T09:54:52.195383Z", + "service": "crawlshot" +} +``` + +--- + +## Web Crawling APIs + +### POST `/api/crawl` + +Initiate a web crawling job to extract HTML content from a URL. + +**Authentication:** Required + +#### Request Parameters + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `url` | string | ✅ | - | Target URL to crawl (max 2048 chars) | +| `timeout` | integer | ❌ | 30 | Request timeout in seconds (5-300) | +| `delay` | integer | ❌ | 0 | Wait time before capture in milliseconds (0-30000) | +| `block_ads` | boolean | ❌ | true | Block ads using EasyList filters | +| `block_cookie_banners` | boolean | ❌ | true | Block cookie consent banners | +| `block_trackers` | boolean | ❌ | true | Block tracking scripts | +| `wait_until_network_idle` | boolean | ❌ | false | Wait for network activity to cease | + +#### Request Example + +```bash +curl -X POST "https://crawlshot.test/api/crawl" \ + -H "Accept: application/json" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer 1|rrWUM5ZkmLfGipkm1oIusYX45KbukIekUwMjgB3Nd1121a5c" \ + -d '{ + "url": "https://example.com", + "timeout": 30, + "delay": 2000, + "block_ads": true, + "block_cookie_banners": true, + "block_trackers": true, + "wait_until_network_idle": true + }' +``` + +#### Response Example + +```json +{ + "uuid": "b5dc483b-f62d-4e40-8b9e-4715324a8cbb", + "status": "queued", + "message": "Crawl job initiated successfully" +} +``` + +--- + +### GET `/api/crawl/{uuid}` + +Check the status and retrieve results of a crawl job. + +**Authentication:** Required + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `uuid` | string | ✅ | Job UUID returned from crawl initiation | + +#### Request Example + +```bash +curl -X GET "https://crawlshot.test/api/crawl/b5dc483b-f62d-4e40-8b9e-4715324a8cbb" \ + -H "Accept: application/json" \ + -H "Authorization: Bearer 1|rrWUM5ZkmLfGipkm1oIusYX45KbukIekUwMjgB3Nd1121a5c" +``` + +#### Response Examples + +**Queued Status:** +```json +{ + "uuid": "b5dc483b-f62d-4e40-8b9e-4715324a8cbb", + "status": "queued", + "url": "https://example.com", + "created_at": "2025-08-10T10:00:42.000000Z" +} +``` + +**Processing Status:** +```json +{ + "uuid": "b5dc483b-f62d-4e40-8b9e-4715324a8cbb", + "status": "processing", + "url": "https://example.com", + "created_at": "2025-08-10T10:00:42.000000Z", + "started_at": "2025-08-10T10:00:45.000000Z" +} +``` + +**Completed Status:** +```json +{ + "uuid": "b5dc483b-f62d-4e40-8b9e-4715324a8cbb", + "status": "completed", + "url": "https://example.com", + "created_at": "2025-08-10T10:00:42.000000Z", + "started_at": "2025-08-10T10:00:45.000000Z", + "completed_at": "2025-08-10T10:01:12.000000Z", + "result": "\n\n\n Example Domain\n\n\n

Example Domain

\n

This domain is for use in illustrative examples...

\n\n" +} +``` + +**Failed Status:** +```json +{ + "uuid": "b5dc483b-f62d-4e40-8b9e-4715324a8cbb", + "status": "failed", + "url": "https://example.com", + "created_at": "2025-08-10T10:00:42.000000Z", + "started_at": "2025-08-10T10:00:45.000000Z", + "completed_at": "2025-08-10T10:00:50.000000Z", + "error": "Timeout: Navigation failed after 30 seconds" +} +``` + +--- + +### GET `/api/crawl` + +List all crawl jobs with pagination (optional endpoint for debugging). + +**Authentication:** Required + +#### Request Example + +```bash +curl -X GET "https://crawlshot.test/api/crawl" \ + -H "Accept: application/json" \ + -H "Authorization: Bearer 1|rrWUM5ZkmLfGipkm1oIusYX45KbukIekUwMjgB3Nd1121a5c" +``` + +#### Response Example + +```json +{ + "jobs": [ + { + "uuid": "b5dc483b-f62d-4e40-8b9e-4715324a8cbb", + "type": "crawl", + "url": "https://example.com", + "status": "completed", + "created_at": "2025-08-10T10:00:42.000000Z", + "completed_at": "2025-08-10T10:01:12.000000Z" + } + ], + "pagination": { + "current_page": 1, + "total_pages": 5, + "total_items": 100, + "per_page": 20 + } +} +``` + +--- + +## Screenshot APIs + +### POST `/api/shot` + +Initiate a screenshot job to capture an image of a webpage. + +**Authentication:** Required + +#### Request Parameters + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `url` | string | ✅ | - | Target URL to screenshot (max 2048 chars) | +| `viewport_width` | integer | ❌ | 1920 | Viewport width in pixels (320-3840) | +| `viewport_height` | integer | ❌ | 1080 | Viewport height in pixels (240-2160) | +| `format` | string | ❌ | "jpg" | Image format: "jpg", "png", "webp" | +| `quality` | integer | ❌ | 90 | Image quality 1-100 (for JPEG/WebP) | +| `timeout` | integer | ❌ | 30 | Request timeout in seconds (5-300) | +| `delay` | integer | ❌ | 0 | Wait time before capture in milliseconds (0-30000) | +| `block_ads` | boolean | ❌ | true | Block ads using EasyList filters | +| `block_cookie_banners` | boolean | ❌ | true | Block cookie consent banners | +| `block_trackers` | boolean | ❌ | true | Block tracking scripts | + +#### Request Example + +```bash +curl -X POST "https://crawlshot.test/api/shot" \ + -H "Accept: application/json" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer 1|rrWUM5ZkmLfGipkm1oIusYX45KbukIekUwMjgB3Nd1121a5c" \ + -d '{ + "url": "https://example.com", + "viewport_width": 1920, + "viewport_height": 1080, + "format": "webp", + "quality": 90, + "timeout": 30, + "delay": 2000, + "block_ads": true, + "block_cookie_banners": true, + "block_trackers": true + }' +``` + +#### Response Example + +```json +{ + "uuid": "fe37d511-99cb-4295-853b-6d484900a851", + "status": "queued", + "message": "Screenshot job initiated successfully" +} +``` + +--- + +### GET `/api/shot/{uuid}` + +Check the status and retrieve results of a screenshot job. When completed, returns base64 image data and download URL. + +**Authentication:** Required + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `uuid` | string | ✅ | Job UUID returned from screenshot initiation | + +#### Request Example + +```bash +curl -X GET "https://crawlshot.test/api/shot/fe37d511-99cb-4295-853b-6d484900a851" \ + -H "Accept: application/json" \ + -H "Authorization: Bearer 1|rrWUM5ZkmLfGipkm1oIusYX45KbukIekUwMjgB3Nd1121a5c" +``` + +#### Response Examples + +**Queued Status:** +```json +{ + "uuid": "fe37d511-99cb-4295-853b-6d484900a851", + "status": "queued", + "url": "https://example.com", + "created_at": "2025-08-10T10:05:42.000000Z" +} +``` + +**Processing Status:** +```json +{ + "uuid": "fe37d511-99cb-4295-853b-6d484900a851", + "status": "processing", + "url": "https://example.com", + "created_at": "2025-08-10T10:05:42.000000Z", + "started_at": "2025-08-10T10:05:45.000000Z" +} +``` + +**Completed Status:** +```json +{ + "uuid": "fe37d511-99cb-4295-853b-6d484900a851", + "status": "completed", + "url": "https://example.com", + "created_at": "2025-08-10T10:05:42.000000Z", + "started_at": "2025-08-10T10:05:45.000000Z", + "completed_at": "2025-08-10T10:06:12.000000Z", + "result": { + "image_data": "iVBORw0KGgoAAAANSUhEUgAAAHgAAAAyCAYAAACXpx/Y...", + "download_url": "https://crawlshot.test/api/shot/fe37d511-99cb-4295-853b-6d484900a851/download", + "mime_type": "image/webp", + "format": "webp", + "width": 1920, + "height": 1080, + "size": 45678 + } +} +``` + +**Failed Status:** +```json +{ + "uuid": "fe37d511-99cb-4295-853b-6d484900a851", + "status": "failed", + "url": "https://example.com", + "created_at": "2025-08-10T10:05:42.000000Z", + "started_at": "2025-08-10T10:05:45.000000Z", + "completed_at": "2025-08-10T10:05:50.000000Z", + "error": "Timeout: Navigation failed after 30 seconds" +} +``` + +--- + +### GET `/api/shot/{uuid}/download` + +Download the screenshot file directly. Returns the actual image file with appropriate headers. + +**Authentication:** Required + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `uuid` | string | ✅ | Job UUID of a completed screenshot job | + +#### Request Example + +```bash +curl -X GET "https://crawlshot.test/api/shot/fe37d511-99cb-4295-853b-6d484900a851/download" \ + -H "Authorization: Bearer 1|rrWUM5ZkmLfGipkm1oIusYX45KbukIekUwMjgB3Nd1121a5c" \ + --output screenshot.webp +``` + +#### Response + +Returns the image file directly with appropriate `Content-Type` headers: +- `Content-Type: image/jpeg` for JPEG files +- `Content-Type: image/png` for PNG files +- `Content-Type: image/webp` for WebP files + +--- + +### GET `/api/shot` + +List all screenshot jobs with pagination (optional endpoint for debugging). + +**Authentication:** Required + +#### Request Example + +```bash +curl -X GET "https://crawlshot.test/api/shot" \ + -H "Accept: application/json" \ + -H "Authorization: Bearer 1|rrWUM5ZkmLfGipkm1oIusYX45KbukIekUwMjgB3Nd1121a5c" +``` + +#### Response Example + +```json +{ + "jobs": [ + { + "uuid": "fe37d511-99cb-4295-853b-6d484900a851", + "type": "shot", + "url": "https://example.com", + "status": "completed", + "created_at": "2025-08-10T10:05:42.000000Z", + "completed_at": "2025-08-10T10:06:12.000000Z" + } + ], + "pagination": { + "current_page": 1, + "total_pages": 3, + "total_items": 50, + "per_page": 20 + } +} +``` + +--- + +## Job Status Flow + +Both crawl and screenshot jobs follow the same status progression: + +1. **`queued`** - Job created and waiting for processing +2. **`processing`** - Job is currently being executed by a worker +3. **`completed`** - Job finished successfully, results available +4. **`failed`** - Job encountered an error and could not complete + +## Error Responses + +### 401 Unauthorized +```json +{ + "message": "Unauthenticated." +} +``` + +### 404 Not Found +```json +{ + "error": "Job not found" +} +``` + +### 422 Validation Error +```json +{ + "message": "The given data was invalid.", + "errors": { + "url": [ + "The url field is required." + ], + "timeout": [ + "The timeout must be between 5 and 300." + ] + } +} +``` + +## Features + +### Ad & Tracker Blocking +- **EasyList Integration**: Automatically downloads and applies EasyList filters +- **Cookie Banner Blocking**: Removes cookie consent prompts +- **Tracker Blocking**: Blocks Google Analytics, Facebook Pixel, and other tracking scripts +- **Custom Domain Blocking**: Blocks common advertising and tracking domains + +### Image Processing +- **Multiple Formats**: Support for JPEG, PNG, and WebP +- **Quality Control**: Adjustable compression quality (1-100) +- **Imagick Integration**: High-quality image processing and format conversion +- **Responsive Sizing**: Custom viewport dimensions up to 4K resolution + +### Storage & Cleanup +- **24-Hour TTL**: All files automatically deleted after 24 hours +- **Scheduled Cleanup**: Daily automated cleanup of expired files +- **Manual Cleanup**: `php artisan crawlshot:prune-storage` command available + +### Performance +- **Background Processing**: All jobs processed asynchronously via Laravel Horizon +- **Queue Management**: Built-in retry logic and failure handling +- **Caching**: EasyList filters cached for optimal performance +- **Monitoring**: Horizon dashboard for real-time job monitoring at `/horizon` + +## Rate Limiting + +API endpoints include rate limiting to prevent abuse. Contact your system administrator for current rate limit settings. + +## Support + +For technical support or questions about the Crawlshot API, please refer to the system documentation or contact your administrator. \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..1b0a8a5 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,262 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Crawlshot is a self-hosted API service built on Laravel 12 that provides web crawling and screenshot capabilities using Spatie Browsershot. It's designed as a self-hosted alternative to services like ScreenshotOne, offering browser automation through a REST API with authentication and job processing. + +### Core Features + +- **Web Crawling**: HTML extraction using headless Chrome via Spatie Browsershot +- **Screenshots**: Image capture using Imagick with customizable dimensions +- **Ad/Tracker Blocking**: Built-in blocking of ads, cookie banners, and trackers +- **Authentication**: Laravel Sanctum API token authentication +- **Job Processing**: Laravel Horizon for background job management +- **Temporary Storage**: 24-hour auto-deletion of crawl results +- **Status Tracking**: UUID-based job status monitoring + +### Technology Stack + +- **Backend**: PHP 8.3+ with Laravel 12 framework +- **Browser Automation**: Spatie Browsershot (Puppeteer/Chrome headless) +- **Queue System**: Laravel Horizon for job processing +- **Authentication**: Laravel Sanctum for API tokens +- **Testing**: Pest PHP testing framework +- **Database**: SQLite (development) for job tracking and API tokens + +## API Endpoints + +### Core API Routes + +``` +POST /api/crawl +- Initiates crawling/screenshot job +- Parameters: url, type (html|image), width, height, timeout +- Returns: {"uuid": "job-uuid", "status": "queued"} + +GET /api/crawl/{uuid} +- Checks job status and retrieves results +- Returns: {"status": "processing|completed|failed", "result": "html content or image url"} +``` + +### Supported Parameters (mapped to Browsershot capabilities) + +**HTML Crawling**: + +- `url`: Target URL to crawl +- `timeout`: Request timeout in seconds (via `timeout()` method) +- `block_ads`: true/false - Uses EasyList filter (https://easylist.to/easylist/easylist.txt) +- `block_cookie_banners`: true/false - Uses cookie banner blocking patterns +- `block_trackers`: true/false - Uses tracker blocking patterns +- `delay`: Wait time before capture in milliseconds (via `setDelay()`) +- `wait_until_network_idle`: Wait for network activity to cease (via `waitUntilNetworkIdle()`) + +**Screenshot Capture**: + +- `url`: Target URL to screenshot +- `viewport_width`: Viewport width (via `windowSize()` method) +- `viewport_height`: Viewport height (via `windowSize()` method) +- `format`: jpg, png, webp (via Imagick post-processing) +- `quality`: Image quality 1-100 for JPEG (via `setScreenshotType('jpeg', quality)`) +- `block_ads`: true/false - Uses EasyList filter for ad blocking +- `block_cookie_banners`: true/false - Uses cookie banner blocking patterns +- `block_trackers`: true/false - Uses tracker blocking patterns +- `timeout`: Request timeout in seconds (via `timeout()` method) +- `delay`: Wait time before capture in milliseconds (via `setDelay()`) + +## Development Commands + +### Starting the Development Environment + +User will start the development, do not start yourself, prompt the user to start instead + +### Queue Management with Horizon + +User will star the horizon, do not start yourself, prompt the user to start instead + +# Horizon dashboard available at: /horizon + +# Monitor job queues, failed jobs, and metrics + +```` + +### Individual Services +Do not start them yourself, prompt the user to start instead + +### Testing +```bash +# Run all tests using Pest +composer run test + +# Run API endpoint tests +php artisan test --filter=Api + +# Test browsershot functionality +php artisan test tests/Feature/BrowsershotTest.php +```` + +### Database Operations + +Never run database migrations yourself, prompt the user to run instead + +### API Token Management + +```bash +# Generate API tokens via Tinker +php artisan tinker +# User::find(1)->createToken('client-name')->plainTextToken + +# Prune expired tokens +php artisan sanctum:prune-expired --hours=24 +``` + +### Storage Management +```bash +# Prune expired crawl results (HTML and images older than 24 hours) +php artisan crawlshot:prune-storage + +# Run storage cleanup via scheduled job +php artisan schedule:run +``` + +### Browsershot Setup Requirements + +```bash +# Install Node.js and Puppeteer dependencies +npm install puppeteer + +# For production servers, ensure Chrome/Chromium is installed +# Ubuntu/Debian: apt-get install chromium-browser +# Alpine: apk add chromium +# Or use Puppeteer's bundled Chromium +``` + +## Architecture Overview + +### Job Processing Flow + +1. **Crawl API Request** → `/api/crawl` with URL and parameters +2. ** Screenshot API Request** → `/api/shot` with URL and parameters +3. **Job Creation** → Queue job with UUID, store in database +4. **Processing** → Horizon worker uses Browsershot to capture content +5. **Storage** → Save HTML/image to storage with 24h expiry +6. **Status Check** → `/api/crawl/{uuid}` returns result when ready + +### Directory Structure + +``` +app/ +├── Http/Controllers/Api/ +│ └── CrawlController.php # Main API endpoints (/crawl, /crawl/{uuid}) +│ └── ShotController.php # Main API endpoints (/shot, /shot/{uuid}) + +├── Jobs/ +│ ├── ProcessCrawlShotJob.php # Browsershot integration +│ └── CleanupOldResults.php # Auto-delete expired files +├── Models/ +│ ├── CrawlShotJob.php # Job tracking model +│ └── User.php # API token authentication +└── Services/ + ├── BrowsershotService.php # Browsershot wrapper with filtering + └── EasyListService.php # ProtonMail php-adblock-parser wrapper + +storage/app/crawlshot/ # Temporary result storage (24h TTL) +├── html/ # HTML crawl results +└── images/ # Screenshot files (JPEG/PNG/WebP) + +routes/ +└── api.php # /crawl endpoints with Sanctum auth +``` + +### Browsershot Configuration + +```php +// Basic screenshot configuration with EasyList ad blocking +$browsershot = Browsershot::url($url) + ->windowSize($width, $height) + ->setScreenshotType('png') // Save as PNG first for Imagick processing + ->setDelay($delayInMs) + ->waitUntilNetworkIdle() + ->timeout($timeoutInSeconds); + +// Apply EasyList filters if block_ads is true +if ($blockAds) { + $blockedDomains = EasyListService::getBlockedDomains($url); + $blockedUrls = EasyListService::getBlockedUrls($url); + $browsershot->blockDomains($blockedDomains)->blockUrls($blockedUrls); +} + +$tempPath = storage_path('temp_screenshot.png'); +$browsershot->save($tempPath); + +// Convert to desired format using Imagick if needed +if ($format === 'webp') { + $imagick = new Imagick($tempPath); + $imagick->setImageFormat('webp'); + $imagick->writeImage($finalPath); + unlink($tempPath); +} + +// HTML crawling configuration with EasyList filtering +$browsershot = Browsershot::url($url) + ->setDelay($delayInMs) + ->waitUntilNetworkIdle() + ->timeout($timeoutInSeconds); + +// Apply EasyList filters if block_ads is true +if ($blockAds) { + $blockedDomains = EasyListService::getBlockedDomains($url); + $blockedUrls = EasyListService::getBlockedUrls($url); + $browsershot->blockDomains($blockedDomains)->blockUrls($blockedUrls); +} + +$html = $browsershot->bodyHtml(); +``` + +### Job States + +- **queued**: Job created, waiting for processing +- **processing**: Horizon worker running Browsershot +- **completed**: Result stored, available via status endpoint +- **failed**: Browsershot error, timeout, or invalid URL + +### Storage Strategy + +- HTML results: `storage/app/crawlshot/html/{uuid}.html` +- Image results: `storage/app/crawlshot/images/{uuid}.jpg`, `.png`, or `.webp` +- Auto-cleanup scheduled job removes files after 24 hours +- Database tracks job metadata and file paths + +### Authentication & Security + +- All API endpoints protected by Sanctum middleware +- Bearer token required in Authorization header +- Rate limiting on crawl endpoints to prevent abuse +- Input validation for URLs and parameters + +### System Requirements + +- PHP 8.3+ with extensions: gd, imagick (required for WebP format) +- Node.js and npm for Puppeteer +- Chrome/Chromium browser (headless) +- Sufficient disk space for temporary file storage +- Memory for concurrent Browsershot processes + +### EasyList Integration + +- Uses ProtonMail's php-adblock-parser (https://github.com/ProtonMail/php-adblock-parser) +- Service downloads and caches EasyList filters from https://easylist.to/easylist/easylist.txt +- php-adblock-parser handles filter parsing and URL matching +- Filters converted to domains/URLs for `blockDomains()` and `blockUrls()` methods +- Cache updated periodically to maintain current ad blocking effectiveness +- Cookie banner and tracker blocking use additional filter lists (EasyList Cookie, Fanboy's Annoyance) + +### Development Notes + +- Horizon required for proper queue processing +- Chrome/Chromium must be accessible to PHP process +- Consider Docker for consistent browser environment +- Monitor disk usage due to temporary file storage +- EasyList filters cached locally for performance using php-adblock-parser +- Test with various websites for ad/tracker blocking effectiveness diff --git a/Crawlshot API/Health Check.bru b/Crawlshot API/Health Check.bru new file mode 100644 index 0000000..e7dd823 --- /dev/null +++ b/Crawlshot API/Health Check.bru @@ -0,0 +1,19 @@ +meta { + name: Health Check + type: http + seq: 1 +} + +get { + url: {{base_url}}/api/health + body: none + auth: bearer +} + +headers { + Accept: application/json +} + +auth:bearer { + token: {{api_token}} +} diff --git a/Crawlshot API/Screenshots/Download Screenshot File.bru b/Crawlshot API/Screenshots/Download Screenshot File.bru new file mode 100644 index 0000000..00e8869 --- /dev/null +++ b/Crawlshot API/Screenshots/Download Screenshot File.bru @@ -0,0 +1,15 @@ +meta { + name: Download Screenshot File + type: http + seq: 3 +} + +get { + url: {{base_url}}/api/shot/:uuid/download + body: none + auth: bearer +} + +auth:bearer { + token: {{api_token}} +} diff --git a/Crawlshot API/Screenshots/Get Screenshot Status - Results.bru b/Crawlshot API/Screenshots/Get Screenshot Status - Results.bru new file mode 100644 index 0000000..4874cf6 --- /dev/null +++ b/Crawlshot API/Screenshots/Get Screenshot Status - Results.bru @@ -0,0 +1,19 @@ +meta { + name: Get Screenshot Status - Results + type: http + seq: 2 +} + +get { + url: {{base_url}}/api/shot/:uuid + body: none + auth: bearer +} + +headers { + Accept: application/json +} + +auth:bearer { + token: {{api_token}} +} diff --git a/Crawlshot API/Screenshots/Initiate Screenshot Job.bru b/Crawlshot API/Screenshots/Initiate Screenshot Job.bru new file mode 100644 index 0000000..916ff15 --- /dev/null +++ b/Crawlshot API/Screenshots/Initiate Screenshot Job.bru @@ -0,0 +1,35 @@ +meta { + name: Initiate Screenshot Job + type: http + seq: 1 +} + +post { + url: {{base_url}}/api/shot + body: json + auth: bearer +} + +headers { + Accept: application/json + Content-Type: application/json +} + +auth:bearer { + token: {{api_token}} +} + +body:json { + { + "url": "https://example.com", + "viewport_width": 1920, + "viewport_height": 1080, + "format": "webp", + "quality": 90, + "timeout": 30, + "delay": 2000, + "block_ads": true, + "block_cookie_banners": true, + "block_trackers": true + } +} diff --git a/Crawlshot API/Screenshots/List Screenshot Jobs.bru b/Crawlshot API/Screenshots/List Screenshot Jobs.bru new file mode 100644 index 0000000..4c71942 --- /dev/null +++ b/Crawlshot API/Screenshots/List Screenshot Jobs.bru @@ -0,0 +1,19 @@ +meta { + name: List Screenshot Jobs + type: http + seq: 4 +} + +get { + url: {{base_url}}/api/shot + body: none + auth: bearer +} + +headers { + Accept: application/json +} + +auth:bearer { + token: {{api_token}} +} diff --git a/Crawlshot API/Web Crawling/Get Crawl Status - Results.bru b/Crawlshot API/Web Crawling/Get Crawl Status - Results.bru new file mode 100644 index 0000000..b6f7bc0 --- /dev/null +++ b/Crawlshot API/Web Crawling/Get Crawl Status - Results.bru @@ -0,0 +1,19 @@ +meta { + name: Get Crawl Status - Results + type: http + seq: 2 +} + +get { + url: {{base_url}}/api/crawl/:uuid + body: none + auth: bearer +} + +headers { + Accept: application/json +} + +auth:bearer { + token: {{api_token}} +} diff --git a/Crawlshot API/Web Crawling/Initiate Crawl Job.bru b/Crawlshot API/Web Crawling/Initiate Crawl Job.bru new file mode 100644 index 0000000..ebf6d35 --- /dev/null +++ b/Crawlshot API/Web Crawling/Initiate Crawl Job.bru @@ -0,0 +1,32 @@ +meta { + name: Initiate Crawl Job + type: http + seq: 1 +} + +post { + url: {{base_url}}/api/crawl + body: json + auth: bearer +} + +headers { + Accept: application/json + Content-Type: application/json +} + +auth:bearer { + token: {{api_token}} +} + +body:json { + { + "url": "https://example.com", + "timeout": 30, + "delay": 2000, + "block_ads": true, + "block_cookie_banners": true, + "block_trackers": true, + "wait_until_network_idle": true + } +} diff --git a/Crawlshot API/Web Crawling/List Crawl Jobs.bru b/Crawlshot API/Web Crawling/List Crawl Jobs.bru new file mode 100644 index 0000000..92e9185 --- /dev/null +++ b/Crawlshot API/Web Crawling/List Crawl Jobs.bru @@ -0,0 +1,19 @@ +meta { + name: List Crawl Jobs + type: http + seq: 3 +} + +get { + url: {{base_url}}/api/crawl + body: none + auth: bearer +} + +headers { + Accept: application/json +} + +auth:bearer { + token: {{api_token}} +} diff --git a/Crawlshot API/bruno.json b/Crawlshot API/bruno.json new file mode 100644 index 0000000..0bfeb1d --- /dev/null +++ b/Crawlshot API/bruno.json @@ -0,0 +1,9 @@ +{ + "version": "1", + "name": "Crawlshot API", + "type": "collection", + "ignore": [ + "node_modules", + ".git" + ] +} \ No newline at end of file diff --git a/Crawlshot API/environments/DEV.bru b/Crawlshot API/environments/DEV.bru new file mode 100644 index 0000000..02b9d94 --- /dev/null +++ b/Crawlshot API/environments/DEV.bru @@ -0,0 +1,6 @@ +vars { + base_url: https://crawlshot.test +} +vars:secret [ + api_token +] diff --git a/README.md b/README.md index 75c347a..0a7d75e 100644 --- a/README.md +++ b/README.md @@ -1,61 +1,198 @@ -

Laravel Logo

+# Crawlshot -

-Build Status -Total Downloads -Latest Stable Version -License -

+A Laravel web crawling and screenshot service with dual deployment options: -## About Laravel +1. **Standalone API Service** - Full Laravel application with REST API endpoints +2. **Laravel Package** - HTTP client package for use in other Laravel applications -Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as: +## Architecture Overview -- [Simple, fast routing engine](https://laravel.com/docs/routing). -- [Powerful dependency injection container](https://laravel.com/docs/container). -- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage. -- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent). -- Database agnostic [schema migrations](https://laravel.com/docs/migrations). -- [Robust background job processing](https://laravel.com/docs/queues). -- [Real-time event broadcasting](https://laravel.com/docs/broadcasting). +### Standalone API Service +The main Laravel application provides a complete web crawling and screenshot service: -Laravel is accessible, powerful, and provides tools required for large, robust applications. +- **Spatie Browsershot Integration** - Uses Puppeteer for browser automation +- **EasyList Ad Blocking** - Automatic ad/tracker blocking using EasyList filters +- **Queue Processing** - Laravel Horizon for async job processing +- **24-hour Cleanup** - Automatic file and database cleanup +- **Sanctum Authentication** - API token-based authentication +- **SQLite Database** - Stores job metadata and processing status -## Learning Laravel +### Laravel Package +Simple HTTP client package that provides a clean interface to the API: -Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. +- **8 Methods for 8 APIs** - Direct 1:1 mapping to REST endpoints +- **Facade Support** - Clean Laravel integration +- **Auto-discovery** - Automatic service provider registration -You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch. +## Deployment Options -If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library. +### Option 1: Standalone API Service -## Laravel Sponsors +Deploy as a complete Laravel application: -We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com). +```bash +git clone [repository] +cd crawlshot +composer install +npm install puppeteer +php artisan migrate +php artisan serve +``` -### Premium Partners +**API Endpoints:** +- `POST /api/crawl` - Create HTML crawl job +- `GET /api/crawl/{uuid}` - Get crawl status/result +- `GET /api/crawl` - List all crawl jobs +- `POST /api/shot` - Create screenshot job +- `GET /api/shot/{uuid}` - Get screenshot status/result +- `GET /api/shot/{uuid}/download` - Download screenshot file +- `GET /api/shot` - List all screenshot jobs +- `GET /api/health` - Health check -- **[Vehikl](https://vehikl.com)** -- **[Tighten Co.](https://tighten.co)** -- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)** -- **[64 Robots](https://64robots.com)** -- **[Curotec](https://www.curotec.com/services/technologies/laravel)** -- **[DevSquad](https://devsquad.com/hire-laravel-developers)** -- **[Redberry](https://redberry.international/laravel-development)** -- **[Active Logic](https://activelogic.com)** +**Example API Usage:** +```bash +# Create crawl job +curl -X POST "https://crawlshot.test/api/crawl" \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{"url": "https://example.com", "block_ads": true}' -## Contributing +# Check status +curl -H "Authorization: Bearer {token}" \ + "https://crawlshot.test/api/crawl/{uuid}" +``` -Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions). +### Option 2: Laravel Package -## Code of Conduct +Install as a package in your Laravel application: -In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct). +```bash +composer require crawlshot/laravel +php artisan vendor:publish --tag=crawlshot-config +``` -## Security Vulnerabilities +**Configuration:** +```env +CRAWLSHOT_BASE_URL=https://your-crawlshot-api.com +CRAWLSHOT_TOKEN=your-sanctum-token +``` -If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed. +**Package Usage:** +```php +use Crawlshot\Laravel\Facades\Crawlshot; + +// Create crawl job +$response = Crawlshot::createCrawl('https://example.com', [ + 'block_ads' => true, + 'timeout' => 30 +]); + +// Check status +$status = Crawlshot::getCrawlStatus($response['uuid']); + +// Create screenshot +$response = Crawlshot::createShot('https://example.com', [ + 'format' => 'jpg', + 'width' => 1920, + 'height' => 1080 +]); + +// Download screenshot +$imageData = Crawlshot::downloadShot($response['uuid']); +file_put_contents('screenshot.jpg', $imageData); +``` + +## API Reference + +### Available Methods (Package) + +| Method | API Endpoint | Description | +|--------|--------------|-------------| +| `createCrawl(string $url, array $options = [])` | `POST /api/crawl` | Create crawl job | +| `getCrawlStatus(string $uuid)` | `GET /api/crawl/{uuid}` | Get crawl status | +| `listCrawls()` | `GET /api/crawl` | List all crawl jobs | +| `createShot(string $url, array $options = [])` | `POST /api/shot` | Create screenshot job | +| `getShotStatus(string $uuid)` | `GET /api/shot/{uuid}` | Get screenshot status | +| `downloadShot(string $uuid)` | `GET /api/shot/{uuid}/download` | Download screenshot file | +| `listShots()` | `GET /api/shot` | List all screenshot jobs | +| `health()` | `GET /api/health` | Health check | + +### Crawl Options + +```php +[ + 'block_ads' => true, // Block ads using EasyList + 'block_trackers' => true, // Block tracking scripts + 'timeout' => 30, // Request timeout in seconds + 'user_agent' => 'Custom UA', // Custom user agent + 'wait_until' => 'networkidle0' // Wait condition +] +``` + +### Screenshot Options + +```php +[ + 'format' => 'jpg', // jpg, png, webp + 'quality' => 90, // 1-100 for jpg/webp + 'width' => 1920, // Viewport width + 'height' => 1080, // Viewport height + 'full_page' => true, // Capture full page + 'block_ads' => true, // Block ads + 'timeout' => 30 // Request timeout +] +``` + +## Features + +### Core Functionality +- **HTML Crawling** - Extract clean HTML content from web pages +- **Screenshot Capture** - Generate high-quality screenshots (JPG, PNG, WebP) +- **Ad Blocking** - Built-in EasyList integration for ad/tracker blocking +- **Queue Processing** - Async job processing with Laravel Horizon +- **File Management** - Automatic cleanup after 24 hours + +### Technical Features +- **Laravel 12** support with PHP 8.3+ +- **Puppeteer Integration** via Spatie Browsershot +- **Sanctum Authentication** for API security +- **SQLite Database** with migrations +- **Auto-discovery** for package installation +- **Environment Configuration** via .env variables + +## Development + +### Requirements +- PHP 8.3+ +- Laravel 12.0+ +- Node.js with Puppeteer +- SQLite (or other database) +- ImageMagick extension + +### Key Dependencies +- `spatie/browsershot` - Browser automation +- `protonlabs/php-adblock-parser` - EasyList parsing +- `laravel/horizon` - Queue monitoring (standalone) +- `laravel/sanctum` - API authentication (standalone) + +### File Structure + +``` +├── app/ # Laravel application (standalone) +│ ├── Http/Controllers/Api/ # API controllers +│ ├── Jobs/ # Queue jobs +│ ├── Models/ # Eloquent models +│ └── Services/ # Core services +├── src/ # Package source (both modes) +│ ├── CrawlshotClient.php # HTTP client (package mode) +│ ├── CrawlshotServiceProvider.php +│ ├── Facades/Crawlshot.php +│ └── config/crawlshot.php +├── routes/api.php # API routes (standalone) +├── database/migrations/ # Database schema +└── composer.json # Package definition +``` ## License -The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). +MIT \ No newline at end of file diff --git a/app/Console/Commands/CreateApiToken.php b/app/Console/Commands/CreateApiToken.php new file mode 100644 index 0000000..e8243a1 --- /dev/null +++ b/app/Console/Commands/CreateApiToken.php @@ -0,0 +1,31 @@ +argument('name'); + $email = $this->argument('email'); + + $user = User::firstOrCreate(['email' => $email], [ + 'name' => $name, + 'password' => bcrypt('password') + ]); + + $token = $user->createToken('crawlshot-api')->plainTextToken; + + $this->info("API Token created successfully!"); + $this->line("Token: {$token}"); + $this->line("Use this in your Authorization header: Bearer {$token}"); + + return 0; + } +} \ No newline at end of file diff --git a/app/Console/Commands/PruneStorage.php b/app/Console/Commands/PruneStorage.php new file mode 100644 index 0000000..e3596df --- /dev/null +++ b/app/Console/Commands/PruneStorage.php @@ -0,0 +1,100 @@ +option('hours'); + $dryRun = $this->option('dry-run'); + + $this->info("Pruning crawlshot storage files older than {$hours} hours..."); + + if ($dryRun) { + $this->warn('DRY RUN MODE - No files will actually be deleted'); + } + + // Find jobs older than specified hours + $cutoffTime = Carbon::now()->subHours($hours); + + $oldJobs = CrawlShotJob::where('created_at', '<', $cutoffTime) + ->whereNotNull('file_path') + ->get(); + + if ($oldJobs->isEmpty()) { + $this->info('No files found to prune.'); + return Command::SUCCESS; + } + + $this->info("Found {$oldJobs->count()} files to prune:"); + + $deletedFiles = 0; + $deletedRecords = 0; + $errors = 0; + + foreach ($oldJobs as $job) { + $this->line("- {$job->type} job {$job->uuid} ({$job->file_path})"); + + if (!$dryRun) { + // Delete the file if it exists + if ($job->file_path && file_exists($job->file_path)) { + if (unlink($job->file_path)) { + $deletedFiles++; + } else { + $this->error(" Failed to delete file: {$job->file_path}"); + $errors++; + } + } + + // Delete the database record + try { + $job->delete(); + $deletedRecords++; + } catch (\Exception $e) { + $this->error(" Failed to delete database record: {$e->getMessage()}"); + $errors++; + } + } + } + + if (!$dryRun) { + $this->info("Cleanup completed:"); + $this->line(" - Files deleted: {$deletedFiles}"); + $this->line(" - Database records deleted: {$deletedRecords}"); + + if ($errors > 0) { + $this->error(" - Errors encountered: {$errors}"); + return Command::FAILURE; + } + } else { + $this->info("Would have deleted {$oldJobs->count()} files and records"); + } + + return Command::SUCCESS; + } +} diff --git a/app/Http/Controllers/Api/CrawlController.php b/app/Http/Controllers/Api/CrawlController.php new file mode 100644 index 0000000..c4ca3dc --- /dev/null +++ b/app/Http/Controllers/Api/CrawlController.php @@ -0,0 +1,128 @@ +validate([ + 'url' => 'required|url|max:2048', + 'timeout' => 'integer|min:5|max:300', + 'delay' => 'integer|min:0|max:30000', + 'block_ads' => 'boolean', + 'block_cookie_banners' => 'boolean', + 'block_trackers' => 'boolean', + 'wait_until_network_idle' => 'boolean' + ]); + + $uuid = Str::uuid()->toString(); + + $job = CrawlShotJob::create([ + 'uuid' => $uuid, + 'type' => 'crawl', + 'url' => $validated['url'], + 'status' => 'queued', + 'parameters' => array_filter([ + 'timeout' => $validated['timeout'] ?? 30, + 'delay' => $validated['delay'] ?? 0, + 'block_ads' => $validated['block_ads'] ?? true, + 'block_cookie_banners' => $validated['block_cookie_banners'] ?? true, + 'block_trackers' => $validated['block_trackers'] ?? true, + 'wait_until_network_idle' => $validated['wait_until_network_idle'] ?? false + ]) + ]); + + ProcessCrawlShotJob::dispatch($uuid); + + return response()->json([ + 'uuid' => $uuid, + 'status' => 'queued', + 'message' => 'Crawl job initiated successfully' + ], 201); + } + + public function status(string $uuid): JsonResponse + { + $job = CrawlShotJob::where('uuid', $uuid)->first(); + + if (!$job) { + return response()->json(['error' => 'Job not found'], 404); + } + + $response = [ + 'uuid' => $job->uuid, + 'status' => $job->status, + 'url' => $job->url, + 'created_at' => $job->created_at->toISOString() + ]; + + if ($job->started_at) { + $response['started_at'] = $job->started_at->toISOString(); + } + + if ($job->completed_at) { + $response['completed_at'] = $job->completed_at->toISOString(); + } + + if ($job->status === 'completed' && $job->file_path) { + $response['result'] = [ + 'html' => [ + 'url' => url("/api/crawl/{$job->uuid}.html"), + 'raw' => Storage::get($job->file_path) + ] + ]; + } + + if ($job->status === 'failed' && $job->error_message) { + $response['error'] = $job->error_message; + } + + return response()->json($response); + } + + public function index(): JsonResponse + { + $jobs = CrawlShotJob::where('type', 'crawl') + ->orderBy('created_at', 'desc') + ->paginate(20); + + $response = [ + 'jobs' => $jobs->items(), + 'pagination' => [ + 'current_page' => $jobs->currentPage(), + 'total_pages' => $jobs->lastPage(), + 'total_items' => $jobs->total(), + 'per_page' => $jobs->perPage() + ] + ]; + + return response()->json($response); + } + + public function serve(string $uuid): Response + { + $job = CrawlShotJob::where('uuid', $uuid)->where('type', 'crawl')->first(); + + if (!$job || $job->status !== 'completed') { + return response('HTML file not found or not ready', 404); + } + + if (!$job->file_path || !Storage::exists($job->file_path)) { + return response('HTML file not found', 404); + } + + return response(Storage::get($job->file_path)) + ->header('Content-Type', 'text/html; charset=utf-8'); + } +} diff --git a/app/Http/Controllers/Api/ShotController.php b/app/Http/Controllers/Api/ShotController.php new file mode 100644 index 0000000..1ac788a --- /dev/null +++ b/app/Http/Controllers/Api/ShotController.php @@ -0,0 +1,151 @@ +validate([ + 'url' => 'required|url|max:2048', + 'viewport_width' => 'integer|min:320|max:3840', + 'viewport_height' => 'integer|min:240|max:2160', + 'quality' => 'integer|min:1|max:100', + 'timeout' => 'integer|min:5|max:300', + 'delay' => 'integer|min:0|max:30000', + 'block_ads' => 'boolean', + 'block_cookie_banners' => 'boolean', + 'block_trackers' => 'boolean' + ]); + + $uuid = Str::uuid()->toString(); + + $job = CrawlShotJob::create([ + 'uuid' => $uuid, + 'type' => 'shot', + 'url' => $validated['url'], + 'status' => 'queued', + 'parameters' => array_filter([ + 'viewport_width' => $validated['viewport_width'] ?? 1920, + 'viewport_height' => $validated['viewport_height'] ?? 1080, + 'format' => 'webp', // Force WebP for all screenshots + 'quality' => $validated['quality'] ?? 90, + 'timeout' => $validated['timeout'] ?? 30, + 'delay' => $validated['delay'] ?? 0, + 'block_ads' => $validated['block_ads'] ?? true, + 'block_cookie_banners' => $validated['block_cookie_banners'] ?? true, + 'block_trackers' => $validated['block_trackers'] ?? true + ]) + ]); + + ProcessCrawlShotJob::dispatch($uuid); + + return response()->json([ + 'uuid' => $uuid, + 'status' => 'queued', + 'message' => 'Screenshot job initiated successfully' + ], 201); + } + + public function status(string $uuid): JsonResponse + { + $job = CrawlShotJob::where('uuid', $uuid)->first(); + + if (!$job) { + return response()->json(['error' => 'Job not found'], 404); + } + + $response = [ + 'uuid' => $job->uuid, + 'status' => $job->status, + 'url' => $job->url, + 'created_at' => $job->created_at->toISOString() + ]; + + if ($job->started_at) { + $response['started_at'] = $job->started_at->toISOString(); + } + + if ($job->completed_at) { + $response['completed_at'] = $job->completed_at->toISOString(); + } + + if ($job->status === 'completed' && $job->file_path) { + $imageData = Storage::get($job->file_path); + + $response['result'] = [ + 'image' => [ + 'url' => url("/api/shot/{$job->uuid}.webp"), + 'raw' => base64_encode($imageData), + ], + 'mime_type' => 'image/webp', + 'format' => 'webp', + 'width' => $job->parameters['viewport_width'] ?? 1920, + 'height' => $job->parameters['viewport_height'] ?? 1080, + 'size' => strlen($imageData) + ]; + } + + if ($job->status === 'failed' && $job->error_message) { + $response['error'] = $job->error_message; + } + + return response()->json($response); + } + + public function serve(string $uuid): Response + { + $job = CrawlShotJob::where('uuid', $uuid)->where('type', 'shot')->first(); + + if (!$job || $job->status !== 'completed') { + return response('Screenshot not found or not ready', 404); + } + + if (!$job->file_path || !Storage::exists($job->file_path)) { + return response('Screenshot file not found', 404); + } + + // Always serve as WebP + return response(Storage::get($job->file_path)) + ->header('Content-Type', 'image/webp'); + } + + public function index(): JsonResponse + { + $jobs = CrawlShotJob::where('type', 'shot') + ->orderBy('created_at', 'desc') + ->paginate(20); + + $response = [ + 'jobs' => $jobs->items(), + 'pagination' => [ + 'current_page' => $jobs->currentPage(), + 'total_pages' => $jobs->lastPage(), + 'total_items' => $jobs->total(), + 'per_page' => $jobs->perPage() + ] + ]; + + return response()->json($response); + } + + private function getMimeType(string $format): string + { + $mimeTypes = [ + 'jpg' => 'image/jpeg', + 'png' => 'image/png', + 'webp' => 'image/webp' + ]; + + return $mimeTypes[$format] ?? 'image/webp'; + } +} diff --git a/app/Jobs/CleanupOldResults.php b/app/Jobs/CleanupOldResults.php new file mode 100644 index 0000000..59189e1 --- /dev/null +++ b/app/Jobs/CleanupOldResults.php @@ -0,0 +1,43 @@ +subHours(24); + + $oldJobs = CrawlShotJob::where('created_at', '<', $cutoffTime)->get(); + + $deletedFiles = 0; + $deletedJobs = 0; + + foreach ($oldJobs as $job) { + if ($job->file_path && Storage::exists($job->file_path)) { + Storage::delete($job->file_path); + $deletedFiles++; + } + + $job->delete(); + $deletedJobs++; + } + + Log::info("Cleanup completed", [ + 'deleted_files' => $deletedFiles, + 'deleted_jobs' => $deletedJobs, + 'cutoff_time' => $cutoffTime->toISOString() + ]); + } +} \ No newline at end of file diff --git a/app/Jobs/ProcessCrawlShotJob.php b/app/Jobs/ProcessCrawlShotJob.php new file mode 100644 index 0000000..e14d348 --- /dev/null +++ b/app/Jobs/ProcessCrawlShotJob.php @@ -0,0 +1,88 @@ +jobUuid = $jobUuid; + } + + public function handle(): void + { + $job = CrawlShotJob::where('uuid', $this->jobUuid)->first(); + + if (!$job) { + Log::error("CrawlShotJob not found: {$this->jobUuid}"); + return; + } + + try { + $job->update([ + 'status' => 'processing', + 'started_at' => now() + ]); + + $browsershot = new BrowsershotService(); + + if ($job->type === 'crawl') { + $result = $browsershot->crawlHtml($job->url, $job->parameters ?? []); + $this->saveCrawlResult($job, $result); + } elseif ($job->type === 'shot') { + $result = $browsershot->takeScreenshot($job->url, $job->parameters ?? []); + $this->saveScreenshotResult($job, $result); + } + + $job->update([ + 'status' => 'completed', + 'completed_at' => now() + ]); + + } catch (\Exception $e) { + Log::error("Job {$this->jobUuid} failed: " . $e->getMessage()); + + $job->update([ + 'status' => 'failed', + 'error_message' => $e->getMessage(), + 'completed_at' => now() + ]); + } + } + + private function saveCrawlResult(CrawlShotJob $job, string $html): void + { + $filename = "{$job->uuid}.html"; + $path = "crawlshot/html/{$filename}"; + + Storage::put($path, $html); + + $job->update(['file_path' => $path]); + } + + private function saveScreenshotResult(CrawlShotJob $job, array $result): void + { + $parameters = $job->parameters ?? []; + $format = $parameters['format'] ?? 'jpg'; + $filename = "{$job->uuid}.{$format}"; + $path = "crawlshot/images/{$filename}"; + + Storage::put($path, $result['data']); + + $job->update(['file_path' => $path]); + } +} \ No newline at end of file diff --git a/app/Models/CrawlShotJob.php b/app/Models/CrawlShotJob.php new file mode 100644 index 0000000..27a629b --- /dev/null +++ b/app/Models/CrawlShotJob.php @@ -0,0 +1,34 @@ + 'array', + 'started_at' => 'datetime', + 'completed_at' => 'datetime' + ]; + + public function getRouteKeyName() + { + return 'uuid'; + } +} \ No newline at end of file diff --git a/app/Models/User.php b/app/Models/User.php index 749c7b7..a6ab88e 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -6,11 +6,12 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; +use Laravel\Sanctum\HasApiTokens; class User extends Authenticatable { /** @use HasFactory<\Database\Factories\UserFactory> */ - use HasFactory, Notifiable; + use HasFactory, Notifiable, HasApiTokens; /** * The attributes that are mass assignable. diff --git a/app/Providers/HorizonServiceProvider.php b/app/Providers/HorizonServiceProvider.php new file mode 100644 index 0000000..59599dc --- /dev/null +++ b/app/Providers/HorizonServiceProvider.php @@ -0,0 +1,36 @@ +email, [ + // + ]); + }); + } +} diff --git a/app/Services/BrowsershotService.php b/app/Services/BrowsershotService.php new file mode 100644 index 0000000..f9c4e98 --- /dev/null +++ b/app/Services/BrowsershotService.php @@ -0,0 +1,76 @@ +configureBrowsershot($url, $options); + + return $browsershot->bodyHtml(); + } + + public function takeScreenshot(string $url, array $options = []): array + { + $browsershot = $this->configureBrowsershot($url, $options); + + // Configure viewport for screenshots + $width = $options['viewport_width'] ?? 1920; + $height = $options['viewport_height'] ?? 1080; + $browsershot->windowSize($width, $height); + + // Always use WebP format + $quality = $options['quality'] ?? 90; + $browsershot->setScreenshotType('webp', $quality); + + $tempPath = storage_path("temp_screenshot_webp." . time() . '.webp'); + $browsershot->save($tempPath); + + $imageData = file_get_contents($tempPath); + unlink($tempPath); + + return [ + 'data' => $imageData, + 'mime_type' => 'image/webp', + 'width' => $width, + 'height' => $height + ]; + } + + private function configureBrowsershot(string $url, array $options = []): Browsershot + { + $browsershot = Browsershot::url($url); + + // Basic configuration + if (isset($options['timeout'])) { + $browsershot->timeout($options['timeout']); + } + + if (isset($options['delay'])) { + $browsershot->setDelay($options['delay']); + } + + if (isset($options['wait_until_network_idle']) && $options['wait_until_network_idle']) { + $browsershot->waitUntilNetworkIdle(); + } + + // Apply ad/tracker blocking + if (($options['block_ads'] ?? true) || ($options['block_trackers'] ?? true)) { + $easyListService = new EasyListService(); + $blockedDomains = $easyListService->getBlockedDomains($url); + $blockedUrls = $easyListService->getBlockedUrls($url); + + if (!empty($blockedDomains)) { + $browsershot->blockDomains($blockedDomains); + } + if (!empty($blockedUrls)) { + $browsershot->blockUrls($blockedUrls); + } + } + + return $browsershot; + } +} \ No newline at end of file diff --git a/app/Services/EasyListService.php b/app/Services/EasyListService.php new file mode 100644 index 0000000..31442ae --- /dev/null +++ b/app/Services/EasyListService.php @@ -0,0 +1,100 @@ +getFilters(); + $domains = []; + + foreach ($filters as $filter) { + if (strpos($filter, '||') === 0 && strpos($filter, '^') !== false) { + $domain = trim(str_replace(['||', '^'], '', $filter)); + if ($this->isValidDomain($domain)) { + $domains[] = $domain; + } + } + } + + return array_slice(array_unique($domains), 0, 100); // Limit to 100 domains + } + + public function getBlockedUrls(string $url): array + { + $filters = $this->getFilters(); + $urls = []; + + foreach ($filters as $filter) { + if (strpos($filter, '||') !== 0 && strpos($filter, '#') !== 0 && strpos($filter, '!') !== 0) { + $cleanFilter = trim($filter); + if (strlen($cleanFilter) > 3 && strpos($cleanFilter, '*') !== false) { + $urls[] = str_replace('*', '', $cleanFilter); + } + } + } + + return array_slice(array_unique($urls), 0, 50); // Limit to 50 URL patterns + } + + private function getFilters(): array + { + return Cache::remember(self::CACHE_KEY, self::CACHE_TTL, function () { + try { + $response = Http::timeout(30)->get(self::EASYLIST_URL); + + if ($response->successful()) { + $content = $response->body(); + $lines = explode("\n", $content); + + $filters = []; + foreach ($lines as $line) { + $line = trim($line); + if (!empty($line) && strpos($line, '!') !== 0) { + $filters[] = $line; + } + } + + Log::info('EasyList filters updated', ['count' => count($filters)]); + return $filters; + } + + Log::warning('Failed to fetch EasyList filters'); + return $this->getFallbackFilters(); + + } catch (\Exception $e) { + Log::error('Error fetching EasyList filters: ' . $e->getMessage()); + return $this->getFallbackFilters(); + } + }); + } + + private function getFallbackFilters(): array + { + return [ + '||googletagmanager.com^', + '||google-analytics.com^', + '||facebook.com/tr^', + '||doubleclick.net^', + '||googlesyndication.com^', + '||amazon-adsystem.com^', + '||adsystem.amazon.com^', + '||googlesyndication.com^', + '||googleadservices.com^' + ]; + } + + private function isValidDomain(string $domain): bool + { + return filter_var($domain, FILTER_VALIDATE_DOMAIN) !== false; + } +} \ No newline at end of file diff --git a/bootstrap/app.php b/bootstrap/app.php index c183276..fe4ae1a 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -7,9 +7,14 @@ return Application::configure(basePath: dirname(__DIR__)) ->withRouting( web: __DIR__.'/../routes/web.php', + api: __DIR__.'/../routes/api.php', commands: __DIR__.'/../routes/console.php', health: '/up', ) + ->withProviders([ + App\Providers\AppServiceProvider::class, + Crawlshot\Laravel\CrawlshotServiceProvider::class, + ]) ->withMiddleware(function (Middleware $middleware): void { // }) diff --git a/bootstrap/providers.php b/bootstrap/providers.php index 38b258d..4e3b440 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -2,4 +2,5 @@ return [ App\Providers\AppServiceProvider::class, + App\Providers\HorizonServiceProvider::class, ]; diff --git a/composer.json b/composer.json index 5f9090c..18743fb 100644 --- a/composer.json +++ b/composer.json @@ -1,16 +1,32 @@ { "$schema": "https://getcomposer.org/schema.json", - "name": "laravel/laravel", - "type": "project", - "description": "The skeleton application for the Laravel framework.", - "keywords": ["laravel", "framework"], + "name": "crawlshot/laravel", + "type": "library", + "description": "Laravel HTTP client package for Crawlshot API - web crawling and screenshot service", + "keywords": [ + "laravel", + "crawler", + "screenshot", + "api-client", + "web-scraping", + "http-client" + ], "license": "MIT", "require": { - "php": "^8.2", - "laravel/framework": "^12.0", - "laravel/tinker": "^2.10.1" + "php": "^8.3", + "illuminate/support": "^10.0|^11.0|^12.0", + "illuminate/http": "^10.0|^11.0|^12.0", + "illuminate/database": "^10.0|^11.0|^12.0", + "illuminate/queue": "^10.0|^11.0|^12.0", + "protonlabs/php-adblock-parser": "^1.1", + "spatie/browsershot": "^5.0", + "ext-imagick": "*" }, "require-dev": { + "laravel/framework": "^12.0", + "laravel/horizon": "^5.33", + "laravel/sanctum": "^4.2", + "laravel/tinker": "^2.10.1", "fakerphp/faker": "^1.23", "laravel/pail": "^1.2.2", "laravel/pint": "^1.13", @@ -18,10 +34,12 @@ "mockery/mockery": "^1.6", "nunomaduro/collision": "^8.6", "pestphp/pest": "^3.8", - "pestphp/pest-plugin-laravel": "^3.2" + "pestphp/pest-plugin-laravel": "^3.2", + "orchestra/testbench": "^10.0" }, "autoload": { "psr-4": { + "Crawlshot\\Laravel\\": "src/", "App\\": "app/", "Database\\Factories\\": "database/factories/", "Database\\Seeders\\": "database/seeders/" @@ -34,8 +52,7 @@ }, "scripts": { "post-autoload-dump": [ - "@php artisan config:clear", - "@php artisan clear-compiled", + "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", "@php artisan package:discover --ansi" ], "post-update-cmd": [ @@ -48,19 +65,16 @@ "@php artisan key:generate --ansi", "@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"", "@php artisan migrate --graceful --ansi" - ], - "dev": [ - "Composer\\Config::disableProcessTimeout", - "npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite" - ], - "test": [ - "@php artisan config:clear --ansi", - "@php artisan test" ] }, "extra": { "laravel": { - "dont-discover": [] + "providers": [ + "Crawlshot\\Laravel\\CrawlshotServiceProvider" + ], + "aliases": { + "Crawlshot": "Crawlshot\\Laravel\\Facades\\Crawlshot" + } } }, "config": { diff --git a/composer.lock b/composer.lock index 80378a7..d9521f3 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "adb4b680144df947b976ddb89b6572b1", + "content-hash": "076f59d93a40ad8d11d4064b11f4fc9b", "packages": [ { "name": "brick/math", @@ -1054,6 +1054,100 @@ ], "time": "2025-02-03T10:55:03+00:00" }, + { + "name": "jeremykendall/php-domain-parser", + "version": "6.4.0", + "source": { + "type": "git", + "url": "https://github.com/jeremykendall/php-domain-parser.git", + "reference": "98401b32371fc1a75d93d4653d311b38e71f0d82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jeremykendall/php-domain-parser/zipball/98401b32371fc1a75d93d4653d311b38e71f0d82", + "reference": "98401b32371fc1a75d93d4653d311b38e71f0d82", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^8.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.65.0", + "guzzlehttp/guzzle": "^7.9.2", + "guzzlehttp/psr7": "^1.6 || ^2.7.0", + "phpstan/phpstan": "^1.12.13", + "phpstan/phpstan-phpunit": "^1.4.2", + "phpstan/phpstan-strict-rules": "^1.6.1", + "phpunit/phpunit": "^10.5.15 || ^11.5.1", + "psr/http-factory": "^1.1.0", + "psr/simple-cache": "^1.0.1 || ^2.0.0", + "symfony/cache": "^v5.0.0 || ^6.4.16", + "symfony/var-dumper": "^v6.4.18 || ^7.2" + }, + "suggest": { + "league/uri": "To parse and extract the host from an URL using a RFC3986/RFC3987 URI parser", + "psr/http-client-implementation": "To use the storage functionality which depends on PSR-18", + "psr/http-factory-implementation": "To use the storage functionality which depends on PSR-17", + "psr/simple-cache-implementation": "To use the storage functionality which depends on PSR-16", + "rowbot/url": "To parse and extract the host from an URL using a WHATWG URL parser", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.x-dev" + } + }, + "autoload": { + "psr-4": { + "Pdp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jeremy Kendall", + "homepage": "https://about.me/jeremykendall", + "role": "Developer" + }, + { + "name": "Ignace Nyamagana Butera", + "homepage": "https://nyamsprod.com", + "role": "Developer" + }, + { + "name": "Contributors", + "homepage": "https://github.com/jeremykendall/php-domain-parser/graphs/contributors" + } + ], + "description": "Public Suffix List and IANA Root Zone Database based Domain parsing implemented in PHP.", + "homepage": "https://github.com/jeremykendall/php-domain-parser", + "keywords": [ + "PSL", + "Public Suffix List", + "Top Level Domains", + "domain parsing", + "iana", + "icann", + "idn", + "tld" + ], + "support": { + "issues": "https://github.com/jeremykendall/php-domain-parser/issues", + "source": "https://github.com/jeremykendall/php-domain-parser" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2025-04-26T11:19:48+00:00" + }, { "name": "laravel/framework", "version": "v12.22.1", @@ -1389,72 +1483,6 @@ }, "time": "2025-03-19T13:51:03+00:00" }, - { - "name": "laravel/tinker", - "version": "v2.10.1", - "source": { - "type": "git", - "url": "https://github.com/laravel/tinker.git", - "reference": "22177cc71807d38f2810c6204d8f7183d88a57d3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laravel/tinker/zipball/22177cc71807d38f2810c6204d8f7183d88a57d3", - "reference": "22177cc71807d38f2810c6204d8f7183d88a57d3", - "shasum": "" - }, - "require": { - "illuminate/console": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", - "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", - "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", - "php": "^7.2.5|^8.0", - "psy/psysh": "^0.11.1|^0.12.0", - "symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0" - }, - "require-dev": { - "mockery/mockery": "~1.3.3|^1.4.2", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^8.5.8|^9.3.3|^10.0" - }, - "suggest": { - "illuminate/database": "The Illuminate Database package (^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0)." - }, - "type": "library", - "extra": { - "laravel": { - "providers": [ - "Laravel\\Tinker\\TinkerServiceProvider" - ] - } - }, - "autoload": { - "psr-4": { - "Laravel\\Tinker\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" - } - ], - "description": "Powerful REPL for the Laravel framework.", - "keywords": [ - "REPL", - "Tinker", - "laravel", - "psysh" - ], - "support": { - "issues": "https://github.com/laravel/tinker/issues", - "source": "https://github.com/laravel/tinker/tree/v2.10.1" - }, - "time": "2025-01-27T14:24:01+00:00" - }, { "name": "league/commonmark", "version": "2.7.1", @@ -2365,64 +2393,6 @@ }, "time": "2025-08-06T21:43:34+00:00" }, - { - "name": "nikic/php-parser", - "version": "v5.6.0", - "source": { - "type": "git", - "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "221b0d0fdf1369c71047ad1d18bb5880017bbc56" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/221b0d0fdf1369c71047ad1d18bb5880017bbc56", - "reference": "221b0d0fdf1369c71047ad1d18bb5880017bbc56", - "shasum": "" - }, - "require": { - "ext-ctype": "*", - "ext-json": "*", - "ext-tokenizer": "*", - "php": ">=7.4" - }, - "require-dev": { - "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^9.0" - }, - "bin": [ - "bin/php-parse" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.0-dev" - } - }, - "autoload": { - "psr-4": { - "PhpParser\\": "lib/PhpParser" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Nikita Popov" - } - ], - "description": "A PHP parser written in PHP", - "keywords": [ - "parser", - "php" - ], - "support": { - "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.0" - }, - "time": "2025-07-27T20:03:57+00:00" - }, { "name": "nunomaduro/termwind", "version": "v2.3.1", @@ -2585,6 +2555,60 @@ ], "time": "2024-07-20T21:41:07+00:00" }, + { + "name": "protonlabs/php-adblock-parser", + "version": "1.1.5", + "source": { + "type": "git", + "url": "https://github.com/ProtonMail/php-adblock-parser.git", + "reference": "0a595617e86106b0096c9c395691e3303ed85285" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ProtonMail/php-adblock-parser/zipball/0a595617e86106b0096c9c395691e3303ed85285", + "reference": "0a595617e86106b0096c9c395691e3303ed85285", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-iconv": "*", + "jeremykendall/php-domain-parser": "^6.2", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "type": "project", + "autoload": { + "psr-4": { + "ProtonLabs\\AdblockParser\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Limon Monte", + "email": "limon.monte@gmail.com" + }, + { + "name": "Cyril van Schreven", + "email": "cyril.schreven@protonmail.com" + } + ], + "description": "PHP parser for Adblock Plus filters. A fork of abandoned limonte/php-adblock-parser", + "homepage": "https://github.com/limonte/php-adblock-parser", + "keywords": [ + "adblock", + "parser" + ], + "support": { + "source": "https://github.com/ProtonMail/php-adblock-parser/tree/1.1.5" + }, + "time": "2023-04-03T09:39:07+00:00" + }, { "name": "psr/clock", "version": "1.0.0", @@ -2997,84 +3021,6 @@ }, "time": "2021-10-29T13:26:27+00:00" }, - { - "name": "psy/psysh", - "version": "v0.12.10", - "source": { - "type": "git", - "url": "https://github.com/bobthecow/psysh.git", - "reference": "6e80abe6f2257121f1eb9a4c55bf29d921025b22" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/6e80abe6f2257121f1eb9a4c55bf29d921025b22", - "reference": "6e80abe6f2257121f1eb9a4c55bf29d921025b22", - "shasum": "" - }, - "require": { - "ext-json": "*", - "ext-tokenizer": "*", - "nikic/php-parser": "^5.0 || ^4.0", - "php": "^8.0 || ^7.4", - "symfony/console": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", - "symfony/var-dumper": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" - }, - "conflict": { - "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" - }, - "require-dev": { - "bamarni/composer-bin-plugin": "^1.2" - }, - "suggest": { - "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", - "ext-pdo-sqlite": "The doc command requires SQLite to work.", - "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well." - }, - "bin": [ - "bin/psysh" - ], - "type": "library", - "extra": { - "bamarni-bin": { - "bin-links": false, - "forward-command": false - }, - "branch-alias": { - "dev-main": "0.12.x-dev" - } - }, - "autoload": { - "files": [ - "src/functions.php" - ], - "psr-4": { - "Psy\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Justin Hileman", - "email": "justin@justinhileman.info" - } - ], - "description": "An interactive shell for modern PHP.", - "homepage": "https://psysh.org", - "keywords": [ - "REPL", - "console", - "interactive", - "shell" - ], - "support": { - "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.10" - }, - "time": "2025-08-04T12:39:37+00:00" - }, { "name": "ralouphie/getallheaders", "version": "3.0.3", @@ -3273,6 +3219,135 @@ }, "time": "2025-06-25T14:20:11+00:00" }, + { + "name": "spatie/browsershot", + "version": "5.0.10", + "source": { + "type": "git", + "url": "https://github.com/spatie/browsershot.git", + "reference": "9e5ae15487b3cdc3eb03318c1c8ac38971f60e58" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/browsershot/zipball/9e5ae15487b3cdc3eb03318c1c8ac38971f60e58", + "reference": "9e5ae15487b3cdc3eb03318c1c8ac38971f60e58", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "ext-json": "*", + "php": "^8.2", + "spatie/temporary-directory": "^2.0", + "symfony/process": "^6.0|^7.0" + }, + "require-dev": { + "pestphp/pest": "^3.0", + "spatie/image": "^3.6", + "spatie/pdf-to-text": "^1.52", + "spatie/phpunit-snapshot-assertions": "^4.2.3|^5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\Browsershot\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://github.com/freekmurze", + "role": "Developer" + } + ], + "description": "Convert a webpage to an image or pdf using headless Chrome", + "homepage": "https://github.com/spatie/browsershot", + "keywords": [ + "chrome", + "convert", + "headless", + "image", + "pdf", + "puppeteer", + "screenshot", + "webpage" + ], + "support": { + "source": "https://github.com/spatie/browsershot/tree/5.0.10" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2025-05-15T07:10:57+00:00" + }, + { + "name": "spatie/temporary-directory", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/temporary-directory.git", + "reference": "580eddfe9a0a41a902cac6eeb8f066b42e65a32b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/temporary-directory/zipball/580eddfe9a0a41a902cac6eeb8f066b42e65a32b", + "reference": "580eddfe9a0a41a902cac6eeb8f066b42e65a32b", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\TemporaryDirectory\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alex Vanderbist", + "email": "alex@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "Easily create, use and destroy temporary directories", + "homepage": "https://github.com/spatie/temporary-directory", + "keywords": [ + "php", + "spatie", + "temporary-directory" + ], + "support": { + "issues": "https://github.com/spatie/temporary-directory/issues", + "source": "https://github.com/spatie/temporary-directory/tree/2.3.0" + }, + "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + }, + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2025-01-13T13:04:43+00:00" + }, { "name": "symfony/clock", "version": "v7.3.0", @@ -5917,6 +5992,87 @@ ], "time": "2025-03-05T08:29:11+00:00" }, + { + "name": "composer/semver", + "version": "3.4.3", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.3" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-09-19T14:15:21+00:00" + }, { "name": "doctrine/deprecations", "version": "1.1.5", @@ -6271,6 +6427,86 @@ }, "time": "2025-03-19T14:43:43+00:00" }, + { + "name": "laravel/horizon", + "version": "v5.33.2", + "source": { + "type": "git", + "url": "https://github.com/laravel/horizon.git", + "reference": "baa36725ed24dbbcd7ddb4ba3dcfd4c0f22028ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/horizon/zipball/baa36725ed24dbbcd7ddb4ba3dcfd4c0f22028ed", + "reference": "baa36725ed24dbbcd7ddb4ba3dcfd4c0f22028ed", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-pcntl": "*", + "ext-posix": "*", + "illuminate/contracts": "^9.21|^10.0|^11.0|^12.0", + "illuminate/queue": "^9.21|^10.0|^11.0|^12.0", + "illuminate/support": "^9.21|^10.0|^11.0|^12.0", + "nesbot/carbon": "^2.17|^3.0", + "php": "^8.0", + "ramsey/uuid": "^4.0", + "symfony/console": "^6.0|^7.0", + "symfony/error-handler": "^6.0|^7.0", + "symfony/polyfill-php83": "^1.28", + "symfony/process": "^6.0|^7.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0", + "phpstan/phpstan": "^1.10|^2.0", + "phpunit/phpunit": "^9.0|^10.4|^11.5|^12.0", + "predis/predis": "^1.1|^2.0|^3.0" + }, + "suggest": { + "ext-redis": "Required to use the Redis PHP driver.", + "predis/predis": "Required when not using the Redis PHP driver (^1.1|^2.0|^3.0)." + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Horizon": "Laravel\\Horizon\\Horizon" + }, + "providers": [ + "Laravel\\Horizon\\HorizonServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "6.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Horizon\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Dashboard and code-driven configuration for Laravel queues.", + "keywords": [ + "laravel", + "queue" + ], + "support": { + "issues": "https://github.com/laravel/horizon/issues", + "source": "https://github.com/laravel/horizon/tree/v5.33.2" + }, + "time": "2025-08-05T02:30:15+00:00" + }, { "name": "laravel/pail", "version": "v1.2.3", @@ -6482,6 +6718,136 @@ }, "time": "2025-07-04T16:17:06+00:00" }, + { + "name": "laravel/sanctum", + "version": "v4.2.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/sanctum.git", + "reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/fd6df4f79f48a72992e8d29a9c0ee25422a0d677", + "reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677", + "shasum": "" + }, + "require": { + "ext-json": "*", + "illuminate/console": "^11.0|^12.0", + "illuminate/contracts": "^11.0|^12.0", + "illuminate/database": "^11.0|^12.0", + "illuminate/support": "^11.0|^12.0", + "php": "^8.2", + "symfony/console": "^7.0" + }, + "require-dev": { + "mockery/mockery": "^1.6", + "orchestra/testbench": "^9.0|^10.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Sanctum\\SanctumServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Sanctum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.", + "keywords": [ + "auth", + "laravel", + "sanctum" + ], + "support": { + "issues": "https://github.com/laravel/sanctum/issues", + "source": "https://github.com/laravel/sanctum" + }, + "time": "2025-07-09T19:45:24+00:00" + }, + { + "name": "laravel/tinker", + "version": "v2.10.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/tinker.git", + "reference": "22177cc71807d38f2810c6204d8f7183d88a57d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/tinker/zipball/22177cc71807d38f2810c6204d8f7183d88a57d3", + "reference": "22177cc71807d38f2810c6204d8f7183d88a57d3", + "shasum": "" + }, + "require": { + "illuminate/console": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "php": "^7.2.5|^8.0", + "psy/psysh": "^0.11.1|^0.12.0", + "symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0" + }, + "require-dev": { + "mockery/mockery": "~1.3.3|^1.4.2", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8.5.8|^9.3.3|^10.0" + }, + "suggest": { + "illuminate/database": "The Illuminate Database package (^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0)." + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Tinker\\TinkerServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Tinker\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Powerful REPL for the Laravel framework.", + "keywords": [ + "REPL", + "Tinker", + "laravel", + "psysh" + ], + "support": { + "issues": "https://github.com/laravel/tinker/issues", + "source": "https://github.com/laravel/tinker/tree/v2.10.1" + }, + "time": "2025-01-27T14:24:01+00:00" + }, { "name": "mockery/mockery", "version": "1.6.12", @@ -6625,6 +6991,64 @@ ], "time": "2025-08-01T08:46:24+00:00" }, + { + "name": "nikic/php-parser", + "version": "v5.6.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "221b0d0fdf1369c71047ad1d18bb5880017bbc56" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/221b0d0fdf1369c71047ad1d18bb5880017bbc56", + "reference": "221b0d0fdf1369c71047ad1d18bb5880017bbc56", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.0" + }, + "time": "2025-07-27T20:03:57+00:00" + }, { "name": "nunomaduro/collision", "version": "v8.8.2", @@ -6724,6 +7148,418 @@ ], "time": "2025-06-25T02:12:12+00:00" }, + { + "name": "orchestra/canvas", + "version": "v10.0.2", + "source": { + "type": "git", + "url": "https://github.com/orchestral/canvas.git", + "reference": "94f732350e5c6d7136ff7b0fd05a90079dd77deb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/orchestral/canvas/zipball/94f732350e5c6d7136ff7b0fd05a90079dd77deb", + "reference": "94f732350e5c6d7136ff7b0fd05a90079dd77deb", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "composer/semver": "^3.0", + "illuminate/console": "^12.3.0", + "illuminate/database": "^12.3.0", + "illuminate/filesystem": "^12.3.0", + "illuminate/support": "^12.3.0", + "orchestra/canvas-core": "^10.0.1", + "orchestra/sidekick": "^1.1.0", + "orchestra/testbench-core": "^10.1.0", + "php": "^8.2", + "symfony/polyfill-php83": "^1.31", + "symfony/yaml": "^7.2.0" + }, + "require-dev": { + "laravel/framework": "^12.3.0", + "laravel/pint": "^1.21", + "mockery/mockery": "^1.6.12", + "phpstan/phpstan": "^2.1.8", + "phpunit/phpunit": "^11.5.13", + "spatie/laravel-ray": "^1.40.1" + }, + "bin": [ + "canvas" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Orchestra\\Canvas\\LaravelServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Orchestra\\Canvas\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Mior Muhammad Zaki", + "email": "crynobone@gmail.com" + } + ], + "description": "Code Generators for Laravel Applications and Packages", + "support": { + "issues": "https://github.com/orchestral/canvas/issues", + "source": "https://github.com/orchestral/canvas/tree/v10.0.2" + }, + "time": "2025-04-05T16:01:25+00:00" + }, + { + "name": "orchestra/canvas-core", + "version": "v10.0.1", + "source": { + "type": "git", + "url": "https://github.com/orchestral/canvas-core.git", + "reference": "22b6515e7a070e1c45c8a3a9819f8b6cb0234173" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/orchestral/canvas-core/zipball/22b6515e7a070e1c45c8a3a9819f8b6cb0234173", + "reference": "22b6515e7a070e1c45c8a3a9819f8b6cb0234173", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "composer/semver": "^3.0", + "illuminate/console": "^12.0", + "illuminate/support": "^12.0", + "orchestra/sidekick": "^1.0.2", + "php": "^8.2", + "symfony/polyfill-php83": "^1.31" + }, + "require-dev": { + "laravel/framework": "^12.0", + "laravel/pint": "^1.21", + "mockery/mockery": "^1.6.10", + "orchestra/testbench-core": "^10.0", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^11.5.7", + "symfony/yaml": "^7.2" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Orchestra\\Canvas\\Core\\LaravelServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Orchestra\\Canvas\\Core\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Mior Muhammad Zaki", + "email": "crynobone@gmail.com" + } + ], + "description": "Code Generators Builder for Laravel Applications and Packages", + "support": { + "issues": "https://github.com/orchestral/canvas/issues", + "source": "https://github.com/orchestral/canvas-core/tree/v10.0.1" + }, + "time": "2025-02-19T04:17:05+00:00" + }, + { + "name": "orchestra/sidekick", + "version": "v1.2.14", + "source": { + "type": "git", + "url": "https://github.com/orchestral/sidekick.git", + "reference": "0f7d1d96d390e7bf9118f280dfae74b8b2fb0a00" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/orchestral/sidekick/zipball/0f7d1d96d390e7bf9118f280dfae74b8b2fb0a00", + "reference": "0f7d1d96d390e7bf9118f280dfae74b8b2fb0a00", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "php": "^8.1", + "symfony/polyfill-php83": "^1.32" + }, + "require-dev": { + "fakerphp/faker": "^1.21", + "laravel/framework": "^10.48.29|^11.44.7|^12.1.1|^13.0", + "laravel/pint": "^1.4", + "mockery/mockery": "^1.5.1", + "orchestra/testbench-core": "^8.37.0|^9.14.0|^10.0|^11.0", + "phpstan/phpstan": "^2.1.14", + "phpunit/phpunit": "^10.0|^11.0|^12.0", + "symfony/process": "^6.0|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/Eloquent/functions.php", + "src/Http/functions.php", + "src/functions.php" + ], + "psr-4": { + "Orchestra\\Sidekick\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mior Muhammad Zaki", + "email": "crynobone@gmail.com" + } + ], + "description": "Packages Toolkit Utilities and Helpers for Laravel", + "support": { + "issues": "https://github.com/orchestral/sidekick/issues", + "source": "https://github.com/orchestral/sidekick/tree/v1.2.14" + }, + "time": "2025-08-06T23:54:27+00:00" + }, + { + "name": "orchestra/testbench", + "version": "v10.4.0", + "source": { + "type": "git", + "url": "https://github.com/orchestral/testbench.git", + "reference": "36674005fb1b5cddfd953b8c440507394af8695d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/orchestral/testbench/zipball/36674005fb1b5cddfd953b8c440507394af8695d", + "reference": "36674005fb1b5cddfd953b8c440507394af8695d", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "fakerphp/faker": "^1.23", + "laravel/framework": "^12.8.0", + "mockery/mockery": "^1.6.10", + "orchestra/testbench-core": "^10.4.0", + "orchestra/workbench": "^10.0.6", + "php": "^8.2", + "phpunit/phpunit": "^11.5.3|^12.0.1", + "symfony/process": "^7.2", + "symfony/yaml": "^7.2", + "vlucas/phpdotenv": "^5.6.1" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mior Muhammad Zaki", + "email": "crynobone@gmail.com", + "homepage": "https://github.com/crynobone" + } + ], + "description": "Laravel Testing Helper for Packages Development", + "homepage": "https://packages.tools/testbench/", + "keywords": [ + "BDD", + "TDD", + "dev", + "laravel", + "laravel-packages", + "testing" + ], + "support": { + "issues": "https://github.com/orchestral/testbench/issues", + "source": "https://github.com/orchestral/testbench/tree/v10.4.0" + }, + "time": "2025-06-08T23:29:04+00:00" + }, + { + "name": "orchestra/testbench-core", + "version": "v10.5.0", + "source": { + "type": "git", + "url": "https://github.com/orchestral/testbench-core.git", + "reference": "aaa53b1e410cb61863f475602d3952fee15c7e6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/orchestral/testbench-core/zipball/aaa53b1e410cb61863f475602d3952fee15c7e6a", + "reference": "aaa53b1e410cb61863f475602d3952fee15c7e6a", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "orchestra/sidekick": "~1.1.16|^1.2.12", + "php": "^8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-php83": "^1.32" + }, + "conflict": { + "brianium/paratest": "<7.3.0|>=8.0.0", + "laravel/framework": "<12.8.0|>=13.0.0", + "laravel/serializable-closure": "<1.3.0|>=2.0.0 <2.0.3|>=3.0.0", + "nunomaduro/collision": "<8.0.0|>=9.0.0", + "phpunit/phpunit": "<10.5.35|>=11.0.0 <11.5.3|12.0.0|>=12.4.0" + }, + "require-dev": { + "fakerphp/faker": "^1.24", + "laravel/framework": "^12.8.0", + "laravel/pint": "^1.24", + "laravel/serializable-closure": "^1.3|^2.0.4", + "mockery/mockery": "^1.6.10", + "phpstan/phpstan": "^2.1.19", + "phpunit/phpunit": "^10.5.35|^11.5.3|^12.0.1", + "spatie/laravel-ray": "^1.40.2", + "symfony/process": "^7.2.0", + "symfony/yaml": "^7.2.0", + "vlucas/phpdotenv": "^5.6.1" + }, + "suggest": { + "brianium/paratest": "Allow using parallel testing (^7.3).", + "ext-pcntl": "Required to use all features of the console signal trapping.", + "fakerphp/faker": "Allow using Faker for testing (^1.23).", + "laravel/framework": "Required for testing (^12.8.0).", + "mockery/mockery": "Allow using Mockery for testing (^1.6).", + "nunomaduro/collision": "Allow using Laravel style tests output and parallel testing (^8.0).", + "orchestra/testbench-dusk": "Allow using Laravel Dusk for testing (^10.0).", + "phpunit/phpunit": "Allow using PHPUnit for testing (^10.5.35|^11.5.3|^12.0.1).", + "symfony/process": "Required to use Orchestra\\Testbench\\remote function (^7.2).", + "symfony/yaml": "Required for Testbench CLI (^7.2).", + "vlucas/phpdotenv": "Required for Testbench CLI (^5.6.1)." + }, + "bin": [ + "testbench" + ], + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Orchestra\\Testbench\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mior Muhammad Zaki", + "email": "crynobone@gmail.com", + "homepage": "https://github.com/crynobone" + } + ], + "description": "Testing Helper for Laravel Development", + "homepage": "https://packages.tools/testbench", + "keywords": [ + "BDD", + "TDD", + "dev", + "laravel", + "laravel-packages", + "testing" + ], + "support": { + "issues": "https://github.com/orchestral/testbench/issues", + "source": "https://github.com/orchestral/testbench-core" + }, + "time": "2025-08-06T22:14:57+00:00" + }, + { + "name": "orchestra/workbench", + "version": "v10.0.6", + "source": { + "type": "git", + "url": "https://github.com/orchestral/workbench.git", + "reference": "4e8a5a68200971ddb9ce4abf26488838bf5c0812" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/orchestral/workbench/zipball/4e8a5a68200971ddb9ce4abf26488838bf5c0812", + "reference": "4e8a5a68200971ddb9ce4abf26488838bf5c0812", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "fakerphp/faker": "^1.23", + "laravel/framework": "^12.1.1", + "laravel/pail": "^1.2.2", + "laravel/tinker": "^2.10.1", + "nunomaduro/collision": "^8.6", + "orchestra/canvas": "^10.0.2", + "orchestra/sidekick": "^1.1.0", + "orchestra/testbench-core": "^10.2.1", + "php": "^8.2", + "symfony/polyfill-php83": "^1.31", + "symfony/process": "^7.2", + "symfony/yaml": "^7.2" + }, + "require-dev": { + "laravel/pint": "^1.21.2", + "mockery/mockery": "^1.6.12", + "phpstan/phpstan": "^2.1.8", + "phpunit/phpunit": "^11.5.3|^12.0.1", + "spatie/laravel-ray": "^1.40.1" + }, + "suggest": { + "ext-pcntl": "Required to use all features of the console signal trapping." + }, + "type": "library", + "autoload": { + "psr-4": { + "Orchestra\\Workbench\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mior Muhammad Zaki", + "email": "crynobone@gmail.com" + } + ], + "description": "Workbench Companion for Laravel Packages Development", + "keywords": [ + "dev", + "laravel", + "laravel-packages", + "testing" + ], + "support": { + "issues": "https://github.com/orchestral/workbench/issues", + "source": "https://github.com/orchestral/workbench/tree/v10.0.6" + }, + "time": "2025-04-13T01:07:44+00:00" + }, { "name": "pestphp/pest", "version": "v3.8.2", @@ -7898,6 +8734,84 @@ ], "time": "2025-03-23T16:02:11+00:00" }, + { + "name": "psy/psysh", + "version": "v0.12.10", + "source": { + "type": "git", + "url": "https://github.com/bobthecow/psysh.git", + "reference": "6e80abe6f2257121f1eb9a4c55bf29d921025b22" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/6e80abe6f2257121f1eb9a4c55bf29d921025b22", + "reference": "6e80abe6f2257121f1eb9a4c55bf29d921025b22", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "nikic/php-parser": "^5.0 || ^4.0", + "php": "^8.0 || ^7.4", + "symfony/console": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", + "symfony/var-dumper": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" + }, + "conflict": { + "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.2" + }, + "suggest": { + "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", + "ext-pdo-sqlite": "The doc command requires SQLite to work.", + "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well." + }, + "bin": [ + "bin/psysh" + ], + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": false + }, + "branch-alias": { + "dev-main": "0.12.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Psy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Justin Hileman", + "email": "justin@justinhileman.info" + } + ], + "description": "An interactive shell for modern PHP.", + "homepage": "https://psysh.org", + "keywords": [ + "REPL", + "console", + "interactive", + "shell" + ], + "support": { + "issues": "https://github.com/bobthecow/psysh/issues", + "source": "https://github.com/bobthecow/psysh/tree/v0.12.10" + }, + "time": "2025-08-04T12:39:37+00:00" + }, { "name": "sebastian/cli-parser", "version": "3.0.2", @@ -9104,7 +10018,8 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "^8.2" + "php": "^8.3", + "ext-imagick": "*" }, "platform-dev": {}, "plugin-api-version": "2.6.0" diff --git a/config/horizon.php b/config/horizon.php new file mode 100644 index 0000000..5101f6f --- /dev/null +++ b/config/horizon.php @@ -0,0 +1,213 @@ + env('HORIZON_DOMAIN'), + + /* + |-------------------------------------------------------------------------- + | Horizon Path + |-------------------------------------------------------------------------- + | + | This is the URI path where Horizon will be accessible from. Feel free + | to change this path to anything you like. Note that the URI will not + | affect the paths of its internal API that aren't exposed to users. + | + */ + + 'path' => env('HORIZON_PATH', 'horizon'), + + /* + |-------------------------------------------------------------------------- + | Horizon Redis Connection + |-------------------------------------------------------------------------- + | + | This is the name of the Redis connection where Horizon will store the + | meta information required for it to function. It includes the list + | of supervisors, failed jobs, job metrics, and other information. + | + */ + + 'use' => 'default', + + /* + |-------------------------------------------------------------------------- + | Horizon Redis Prefix + |-------------------------------------------------------------------------- + | + | This prefix will be used when storing all Horizon data in Redis. You + | may modify the prefix when you are running multiple installations + | of Horizon on the same server so that they don't have problems. + | + */ + + 'prefix' => env( + 'HORIZON_PREFIX', + Str::slug(env('APP_NAME', 'laravel'), '_').'_horizon:' + ), + + /* + |-------------------------------------------------------------------------- + | Horizon Route Middleware + |-------------------------------------------------------------------------- + | + | These middleware will get attached onto each Horizon route, giving you + | the chance to add your own middleware to this list or change any of + | the existing middleware. Or, you can simply stick with this list. + | + */ + + 'middleware' => ['web'], + + /* + |-------------------------------------------------------------------------- + | Queue Wait Time Thresholds + |-------------------------------------------------------------------------- + | + | This option allows you to configure when the LongWaitDetected event + | will be fired. Every connection / queue combination may have its + | own, unique threshold (in seconds) before this event is fired. + | + */ + + 'waits' => [ + 'redis:default' => 60, + ], + + /* + |-------------------------------------------------------------------------- + | Job Trimming Times + |-------------------------------------------------------------------------- + | + | Here you can configure for how long (in minutes) you desire Horizon to + | persist the recent and failed jobs. Typically, recent jobs are kept + | for one hour while all failed jobs are stored for an entire week. + | + */ + + 'trim' => [ + 'recent' => 60, + 'pending' => 60, + 'completed' => 60, + 'recent_failed' => 10080, + 'failed' => 10080, + 'monitored' => 10080, + ], + + /* + |-------------------------------------------------------------------------- + | Silenced Jobs + |-------------------------------------------------------------------------- + | + | Silencing a job will instruct Horizon to not place the job in the list + | of completed jobs within the Horizon dashboard. This setting may be + | used to fully remove any noisy jobs from the completed jobs list. + | + */ + + 'silenced' => [ + // App\Jobs\ExampleJob::class, + ], + + /* + |-------------------------------------------------------------------------- + | Metrics + |-------------------------------------------------------------------------- + | + | Here you can configure how many snapshots should be kept to display in + | the metrics graph. This will get used in combination with Horizon's + | `horizon:snapshot` schedule to define how long to retain metrics. + | + */ + + 'metrics' => [ + 'trim_snapshots' => [ + 'job' => 24, + 'queue' => 24, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Fast Termination + |-------------------------------------------------------------------------- + | + | When this option is enabled, Horizon's "terminate" command will not + | wait on all of the workers to terminate unless the --wait option + | is provided. Fast termination can shorten deployment delay by + | allowing a new instance of Horizon to start while the last + | instance will continue to terminate each of its workers. + | + */ + + 'fast_termination' => false, + + /* + |-------------------------------------------------------------------------- + | Memory Limit (MB) + |-------------------------------------------------------------------------- + | + | This value describes the maximum amount of memory the Horizon master + | supervisor may consume before it is terminated and restarted. For + | configuring these limits on your workers, see the next section. + | + */ + + 'memory_limit' => 64, + + /* + |-------------------------------------------------------------------------- + | Queue Worker Configuration + |-------------------------------------------------------------------------- + | + | Here you may define the queue worker settings used by your application + | in all environments. These supervisors and settings handle all your + | queued jobs and will be provisioned by Horizon during deployment. + | + */ + + 'defaults' => [ + 'supervisor-1' => [ + 'connection' => 'redis', + 'queue' => ['default'], + 'balance' => 'auto', + 'autoScalingStrategy' => 'time', + 'maxProcesses' => 1, + 'maxTime' => 0, + 'maxJobs' => 0, + 'memory' => 128, + 'tries' => 1, + 'timeout' => 60, + 'nice' => 0, + ], + ], + + 'environments' => [ + 'production' => [ + 'supervisor-1' => [ + 'maxProcesses' => 10, + 'balanceMaxShift' => 1, + 'balanceCooldown' => 3, + ], + ], + + 'local' => [ + 'supervisor-1' => [ + 'maxProcesses' => 3, + ], + ], + ], +]; diff --git a/config/sanctum.php b/config/sanctum.php new file mode 100644 index 0000000..44527d6 --- /dev/null +++ b/config/sanctum.php @@ -0,0 +1,84 @@ + explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( + '%s%s', + 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', + Sanctum::currentApplicationUrlWithPort(), + // Sanctum::currentRequestHost(), + ))), + + /* + |-------------------------------------------------------------------------- + | Sanctum Guards + |-------------------------------------------------------------------------- + | + | This array contains the authentication guards that will be checked when + | Sanctum is trying to authenticate a request. If none of these guards + | are able to authenticate the request, Sanctum will use the bearer + | token that's present on an incoming request for authentication. + | + */ + + 'guard' => ['web'], + + /* + |-------------------------------------------------------------------------- + | Expiration Minutes + |-------------------------------------------------------------------------- + | + | This value controls the number of minutes until an issued token will be + | considered expired. This will override any values set in the token's + | "expires_at" attribute, but first-party sessions are not affected. + | + */ + + 'expiration' => null, + + /* + |-------------------------------------------------------------------------- + | Token Prefix + |-------------------------------------------------------------------------- + | + | Sanctum can prefix new tokens in order to take advantage of numerous + | security scanning initiatives maintained by open source platforms + | that notify developers if they commit tokens into repositories. + | + | See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning + | + */ + + 'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''), + + /* + |-------------------------------------------------------------------------- + | Sanctum Middleware + |-------------------------------------------------------------------------- + | + | When authenticating your first-party SPA with Sanctum you may need to + | customize some of the middleware Sanctum uses while processing the + | request. You may change the middleware listed below as required. + | + */ + + 'middleware' => [ + 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class, + 'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class, + 'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, + ], + +]; diff --git a/crawlshot-api.postman_collection.json b/crawlshot-api.postman_collection.json new file mode 100644 index 0000000..71db7ff --- /dev/null +++ b/crawlshot-api.postman_collection.json @@ -0,0 +1,598 @@ +{ + "info": { + "name": "Crawlshot API", + "description": "Complete API collection for Crawlshot - A self-hosted web crawling and screenshot service built with Laravel and Spatie Browsershot", + "version": "1.0.0", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{api_token}}", + "type": "string" + } + ] + }, + "variable": [ + { + "key": "base_url", + "value": "https://crawlshot.test", + "type": "string" + }, + { + "key": "api_token", + "value": "1|rrWUM5ZkmLfGipkm1oIusYX45KbukIekUwMjgB3Nd1121a5c", + "type": "string" + } + ], + "item": [ + { + "name": "Health Check", + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json", + "type": "text" + } + ], + "url": { + "raw": "{{base_url}}/api/health", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "health" + ] + }, + "description": "Health check endpoint to verify the service is running. No authentication required." + }, + "response": [ + { + "name": "Success", + "originalRequest": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{base_url}}/api/health", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "health" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [], + "cookie": [], + "body": "{\n \"status\": \"healthy\",\n \"timestamp\": \"2025-08-10T09:54:52.195383Z\",\n \"service\": \"crawlshot\"\n}" + } + ] + }, + { + "name": "Web Crawling", + "item": [ + { + "name": "Initiate Crawl Job", + "request": { + "method": "POST", + "header": [ + { + "key": "Accept", + "value": "application/json", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"url\": \"https://example.com\",\n \"timeout\": 30,\n \"delay\": 2000,\n \"block_ads\": true,\n \"block_cookie_banners\": true,\n \"block_trackers\": true,\n \"wait_until_network_idle\": true\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/api/crawl", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "crawl" + ] + }, + "description": "Initiate a web crawling job to extract HTML content from a URL.\n\n**Parameters:**\n- `url` (required): Target URL to crawl\n- `timeout` (optional): Request timeout in seconds (5-300)\n- `delay` (optional): Wait time before capture in milliseconds (0-30000)\n- `block_ads` (optional): Block ads using EasyList filters (default: true)\n- `block_cookie_banners` (optional): Block cookie banners (default: true)\n- `block_trackers` (optional): Block tracking scripts (default: true)\n- `wait_until_network_idle` (optional): Wait for network activity to cease (default: false)" + }, + "response": [ + { + "name": "Success", + "originalRequest": { + "method": "POST", + "header": [ + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"url\": \"https://example.com\",\n \"timeout\": 30,\n \"block_ads\": true\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/api/crawl", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "crawl" + ] + } + }, + "status": "Created", + "code": 201, + "_postman_previewlanguage": "json", + "header": [], + "cookie": [], + "body": "{\n \"uuid\": \"b5dc483b-f62d-4e40-8b9e-4715324a8cbb\",\n \"status\": \"queued\",\n \"message\": \"Crawl job initiated successfully\"\n}" + } + ] + }, + { + "name": "Get Crawl Status & Results", + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json", + "type": "text" + } + ], + "url": { + "raw": "{{base_url}}/api/crawl/:uuid", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "crawl", + ":uuid" + ], + "variable": [ + { + "key": "uuid", + "value": "b5dc483b-f62d-4e40-8b9e-4715324a8cbb", + "description": "Job UUID returned from crawl initiation" + } + ] + }, + "description": "Check the status and retrieve results of a crawl job. When completed, returns the full HTML content." + }, + "response": [ + { + "name": "Queued", + "originalRequest": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{base_url}}/api/crawl/b5dc483b-f62d-4e40-8b9e-4715324a8cbb", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "crawl", + "b5dc483b-f62d-4e40-8b9e-4715324a8cbb" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [], + "cookie": [], + "body": "{\n \"uuid\": \"b5dc483b-f62d-4e40-8b9e-4715324a8cbb\",\n \"status\": \"queued\",\n \"url\": \"https://example.com\",\n \"created_at\": \"2025-08-10T10:00:42.000000Z\"\n}" + }, + { + "name": "Processing", + "originalRequest": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{base_url}}/api/crawl/b5dc483b-f62d-4e40-8b9e-4715324a8cbb", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "crawl", + "b5dc483b-f62d-4e40-8b9e-4715324a8cbb" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [], + "cookie": [], + "body": "{\n \"uuid\": \"b5dc483b-f62d-4e40-8b9e-4715324a8cbb\",\n \"status\": \"processing\",\n \"url\": \"https://example.com\",\n \"created_at\": \"2025-08-10T10:00:42.000000Z\",\n \"started_at\": \"2025-08-10T10:00:45.000000Z\"\n}" + }, + { + "name": "Completed", + "originalRequest": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{base_url}}/api/crawl/b5dc483b-f62d-4e40-8b9e-4715324a8cbb", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "crawl", + "b5dc483b-f62d-4e40-8b9e-4715324a8cbb" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [], + "cookie": [], + "body": "{\n \"uuid\": \"b5dc483b-f62d-4e40-8b9e-4715324a8cbb\",\n \"status\": \"completed\",\n \"url\": \"https://example.com\",\n \"created_at\": \"2025-08-10T10:00:42.000000Z\",\n \"started_at\": \"2025-08-10T10:00:45.000000Z\",\n \"completed_at\": \"2025-08-10T10:01:12.000000Z\",\n \"result\": \"\\n\\n\\n Example Domain\\n\\n\\n

Example Domain

\\n

This domain is for use in illustrative examples...

\\n\\n\"\n}" + }, + { + "name": "Failed", + "originalRequest": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{base_url}}/api/crawl/b5dc483b-f62d-4e40-8b9e-4715324a8cbb", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "crawl", + "b5dc483b-f62d-4e40-8b9e-4715324a8cbb" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [], + "cookie": [], + "body": "{\n \"uuid\": \"b5dc483b-f62d-4e40-8b9e-4715324a8cbb\",\n \"status\": \"failed\",\n \"url\": \"https://example.com\",\n \"created_at\": \"2025-08-10T10:00:42.000000Z\",\n \"started_at\": \"2025-08-10T10:00:45.000000Z\",\n \"completed_at\": \"2025-08-10T10:00:50.000000Z\",\n \"error\": \"Timeout: Navigation failed after 30 seconds\"\n}" + } + ] + }, + { + "name": "List Crawl Jobs", + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json", + "type": "text" + } + ], + "url": { + "raw": "{{base_url}}/api/crawl", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "crawl" + ] + }, + "description": "List all crawl jobs with pagination. Optional endpoint for debugging and monitoring." + }, + "response": [] + } + ] + }, + { + "name": "Screenshots", + "item": [ + { + "name": "Initiate Screenshot Job", + "request": { + "method": "POST", + "header": [ + { + "key": "Accept", + "value": "application/json", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"url\": \"https://example.com\",\n \"viewport_width\": 1920,\n \"viewport_height\": 1080,\n \"format\": \"webp\",\n \"quality\": 90,\n \"timeout\": 30,\n \"delay\": 2000,\n \"block_ads\": true,\n \"block_cookie_banners\": true,\n \"block_trackers\": true\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/api/shot", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "shot" + ] + }, + "description": "Initiate a screenshot job to capture an image of a webpage.\n\n**Parameters:**\n- `url` (required): Target URL to screenshot\n- `viewport_width` (optional): Viewport width in pixels (320-3840, default: 1920)\n- `viewport_height` (optional): Viewport height in pixels (240-2160, default: 1080)\n- `format` (optional): Image format - jpg, png, webp (default: jpg)\n- `quality` (optional): Image quality 1-100 (default: 90)\n- `timeout` (optional): Request timeout in seconds (5-300)\n- `delay` (optional): Wait time before capture in milliseconds (0-30000)\n- `block_ads` (optional): Block ads using EasyList filters (default: true)\n- `block_cookie_banners` (optional): Block cookie banners (default: true)\n- `block_trackers` (optional): Block tracking scripts (default: true)" + }, + "response": [ + { + "name": "Success", + "originalRequest": { + "method": "POST", + "header": [ + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"url\": \"https://example.com\",\n \"viewport_width\": 1280,\n \"viewport_height\": 720,\n \"format\": \"webp\",\n \"block_ads\": true\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/api/shot", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "shot" + ] + } + }, + "status": "Created", + "code": 201, + "_postman_previewlanguage": "json", + "header": [], + "cookie": [], + "body": "{\n \"uuid\": \"fe37d511-99cb-4295-853b-6d484900a851\",\n \"status\": \"queued\",\n \"message\": \"Screenshot job initiated successfully\"\n}" + } + ] + }, + { + "name": "Get Screenshot Status & Results", + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json", + "type": "text" + } + ], + "url": { + "raw": "{{base_url}}/api/shot/:uuid", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "shot", + ":uuid" + ], + "variable": [ + { + "key": "uuid", + "value": "fe37d511-99cb-4295-853b-6d484900a851", + "description": "Job UUID returned from screenshot initiation" + } + ] + }, + "description": "Check the status and retrieve results of a screenshot job. When completed, returns base64 image data and download URL." + }, + "response": [ + { + "name": "Queued", + "originalRequest": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{base_url}}/api/shot/fe37d511-99cb-4295-853b-6d484900a851", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "shot", + "fe37d511-99cb-4295-853b-6d484900a851" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [], + "cookie": [], + "body": "{\n \"uuid\": \"fe37d511-99cb-4295-853b-6d484900a851\",\n \"status\": \"queued\",\n \"url\": \"https://example.com\",\n \"created_at\": \"2025-08-10T10:05:42.000000Z\"\n}" + }, + { + "name": "Completed", + "originalRequest": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{base_url}}/api/shot/fe37d511-99cb-4295-853b-6d484900a851", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "shot", + "fe37d511-99cb-4295-853b-6d484900a851" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [], + "cookie": [], + "body": "{\n \"uuid\": \"fe37d511-99cb-4295-853b-6d484900a851\",\n \"status\": \"completed\",\n \"url\": \"https://example.com\",\n \"created_at\": \"2025-08-10T10:05:42.000000Z\",\n \"started_at\": \"2025-08-10T10:05:45.000000Z\",\n \"completed_at\": \"2025-08-10T10:06:12.000000Z\",\n \"result\": {\n \"image_data\": \"iVBORw0KGgoAAAANSUhEUgAAAHgAAAAyCAYAAACXpx/Y...\",\n \"download_url\": \"https://crawlshot.test/api/shot/fe37d511-99cb-4295-853b-6d484900a851/download\",\n \"mime_type\": \"image/webp\",\n \"format\": \"webp\",\n \"width\": 1920,\n \"height\": 1080,\n \"size\": 45678\n }\n}" + } + ] + }, + { + "name": "Download Screenshot File", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/api/shot/:uuid/download", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "shot", + ":uuid", + "download" + ], + "variable": [ + { + "key": "uuid", + "value": "fe37d511-99cb-4295-853b-6d484900a851", + "description": "Job UUID" + } + ] + }, + "description": "Download the screenshot file directly. This endpoint returns the actual image file with appropriate headers for downloading." + }, + "response": [] + }, + { + "name": "List Screenshot Jobs", + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json", + "type": "text" + } + ], + "url": { + "raw": "{{base_url}}/api/shot", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "shot" + ] + }, + "description": "List all screenshot jobs with pagination. Optional endpoint for debugging and monitoring." + }, + "response": [] + } + ] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] +} \ No newline at end of file diff --git a/database/migrations/2025_08_10_091937_create_personal_access_tokens_table.php b/database/migrations/2025_08_10_091937_create_personal_access_tokens_table.php new file mode 100644 index 0000000..40ff706 --- /dev/null +++ b/database/migrations/2025_08_10_091937_create_personal_access_tokens_table.php @@ -0,0 +1,33 @@ +id(); + $table->morphs('tokenable'); + $table->text('name'); + $table->string('token', 64)->unique(); + $table->text('abilities')->nullable(); + $table->timestamp('last_used_at')->nullable(); + $table->timestamp('expires_at')->nullable()->index(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('personal_access_tokens'); + } +}; diff --git a/database/migrations/2025_08_10_094046_create_crawl_shot_jobs_table.php b/database/migrations/2025_08_10_094046_create_crawl_shot_jobs_table.php new file mode 100644 index 0000000..501eea4 --- /dev/null +++ b/database/migrations/2025_08_10_094046_create_crawl_shot_jobs_table.php @@ -0,0 +1,36 @@ +id(); + $table->uuid('uuid')->unique(); + $table->string('type'); // 'crawl' or 'shot' + $table->string('url'); + $table->string('status')->default('queued'); // queued, processing, completed, failed + $table->json('parameters')->nullable(); // viewport_width, viewport_height, format, timeout, etc. + $table->string('file_path')->nullable(); // path to saved result + $table->text('error_message')->nullable(); + $table->timestamp('started_at')->nullable(); + $table->timestamp('completed_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('crawl_shot_jobs'); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index d01a0ef..d869f1e 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -13,11 +13,8 @@ class DatabaseSeeder extends Seeder */ public function run(): void { - // User::factory(10)->create(); - - User::factory()->create([ - 'name' => 'Test User', - 'email' => 'test@example.com', + $this->call([ + UserSeeder::class, ]); } } diff --git a/database/seeders/UserSeeder.php b/database/seeders/UserSeeder.php new file mode 100644 index 0000000..4057c90 --- /dev/null +++ b/database/seeders/UserSeeder.php @@ -0,0 +1,29 @@ +command->error("Users already exist! This seeder can only be run once."); + return; + } + + $user = User::create([ + 'name' => 'Crawlshot API User', + 'email' => 'api@crawlshot.test', + 'password' => bcrypt('password') + ]); + + $token = $user->createToken('crawlshot-api')->plainTextToken; + + $this->command->info("User created: {$user->email}"); + $this->command->info("API Token: {$token}"); + $this->command->line("Use this token in Authorization header: Bearer {$token}"); + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..bd0e027 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3298 @@ +{ + "name": "crawlshot", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "puppeteer": "^24.16.0" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.0", + "axios": "^1.11.0", + "concurrently": "^9.0.1", + "laravel-vite-plugin": "^2.0.0", + "tailwindcss": "^4.0.0", + "vite": "^7.0.4" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", + "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", + "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", + "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", + "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", + "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", + "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", + "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", + "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", + "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", + "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", + "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", + "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", + "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", + "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", + "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", + "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", + "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", + "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", + "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", + "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", + "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", + "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", + "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", + "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", + "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", + "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "2.10.6", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.6.tgz", + "integrity": "sha512-pHUn6ZRt39bP3698HFQlu2ZHCkS/lPcpv7fVQcGBSzNNygw171UXAKrCUhy+TEMw4lEttOKDgNpb04hwUAJeiQ==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.1", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.2", + "tar-fs": "^3.1.0", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz", + "integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz", + "integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.2.tgz", + "integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz", + "integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz", + "integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz", + "integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz", + "integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz", + "integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz", + "integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz", + "integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz", + "integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz", + "integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz", + "integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz", + "integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz", + "integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz", + "integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.2.tgz", + "integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz", + "integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz", + "integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz", + "integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz", + "integrity": "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "enhanced-resolve": "^5.18.1", + "jiti": "^2.4.2", + "lightningcss": "1.30.1", + "magic-string": "^0.30.17", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.11" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.11.tgz", + "integrity": "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.4.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.11", + "@tailwindcss/oxide-darwin-arm64": "4.1.11", + "@tailwindcss/oxide-darwin-x64": "4.1.11", + "@tailwindcss/oxide-freebsd-x64": "4.1.11", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", + "@tailwindcss/oxide-linux-x64-musl": "4.1.11", + "@tailwindcss/oxide-wasm32-wasi": "4.1.11", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.11.tgz", + "integrity": "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.11.tgz", + "integrity": "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.11.tgz", + "integrity": "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.11.tgz", + "integrity": "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.11.tgz", + "integrity": "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.11.tgz", + "integrity": "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.11.tgz", + "integrity": "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz", + "integrity": "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.11.tgz", + "integrity": "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.11.tgz", + "integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@emnapi/wasi-threads": "^1.0.2", + "@napi-rs/wasm-runtime": "^0.2.11", + "@tybys/wasm-util": "^0.9.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz", + "integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.11.tgz", + "integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.11.tgz", + "integrity": "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.11", + "@tailwindcss/oxide": "4.1.11", + "tailwindcss": "4.1.11" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.2.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz", + "integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "undici-types": "~7.10.0" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "license": "Apache-2.0" + }, + "node_modules/bare-events": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.6.1.tgz", + "integrity": "sha512-AuTJkq9XmE6Vk0FJVNq5QxETrSA/vKHarWVBG5l/JbdCL1prJemiyJqUS0jrlXO0MftuPq4m3YVYhoNc5+aE/g==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/bare-fs": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.6.tgz", + "integrity": "sha512-25RsLF33BqooOEFNdMcEhMpJy8EoR88zSMrnOQOaM3USnOK2VmaJ1uaQEwPA6AQjrv1lXChScosN6CzbwbO9OQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz", + "integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz", + "integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "streamx": "^2.21.0" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/chromium-bidi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-7.2.0.tgz", + "integrity": "sha512-gREyhyBstermK+0RbcJLbFhcQctg92AGgDe/h/taMJEOLRdtSswBAO9KmvltFSQWgM2LrwWu5SIuEUbdm3JsyQ==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concurrently": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.0.tgz", + "integrity": "sha512-IsB/fiXTupmagMW4MNp2lx2cdSN2FfZq78vF90LBB+zZHArbIQZjQtzXCiXnvTxCZSvXanTqFLWBjw2UkLx1SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1475386", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1475386.tgz", + "integrity": "sha512-RQ809ykTfJ+dgj9bftdeL2vRVxASAuGU+I9LEx9Ij5TXU5HrgAQVmzi72VA+mkzscE12uzlRv5/tWWv9R9J1SA==", + "license": "BSD-3-Clause" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", + "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.8", + "@esbuild/android-arm": "0.25.8", + "@esbuild/android-arm64": "0.25.8", + "@esbuild/android-x64": "0.25.8", + "@esbuild/darwin-arm64": "0.25.8", + "@esbuild/darwin-x64": "0.25.8", + "@esbuild/freebsd-arm64": "0.25.8", + "@esbuild/freebsd-x64": "0.25.8", + "@esbuild/linux-arm": "0.25.8", + "@esbuild/linux-arm64": "0.25.8", + "@esbuild/linux-ia32": "0.25.8", + "@esbuild/linux-loong64": "0.25.8", + "@esbuild/linux-mips64el": "0.25.8", + "@esbuild/linux-ppc64": "0.25.8", + "@esbuild/linux-riscv64": "0.25.8", + "@esbuild/linux-s390x": "0.25.8", + "@esbuild/linux-x64": "0.25.8", + "@esbuild/netbsd-arm64": "0.25.8", + "@esbuild/netbsd-x64": "0.25.8", + "@esbuild/openbsd-arm64": "0.25.8", + "@esbuild/openbsd-x64": "0.25.8", + "@esbuild/openharmony-arm64": "0.25.8", + "@esbuild/sunos-x64": "0.25.8", + "@esbuild/win32-arm64": "0.25.8", + "@esbuild/win32-ia32": "0.25.8", + "@esbuild/win32-x64": "0.25.8" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fdir": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jiti": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", + "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/laravel-vite-plugin": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-2.0.0.tgz", + "integrity": "sha512-pnaKHInJgiWpG/g+LmaISHl7D/1s5wnOXnrGiBdt4NOs+tYZRw0v/ZANELGX2/dGgHyEzO+iZ6x4idpoK04z/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "vite-plugin-full-reload": "^1.1.0" + }, + "bin": { + "clean-orphaned-assets": "bin/clean.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^7.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/puppeteer": { + "version": "24.16.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.16.0.tgz", + "integrity": "sha512-5qxFGOpdAzYexoPwKPEF4L/IYKYOFE1MxWsqcp7K33HySM8N8S/yZwSQCaV0rzmJsTLX5LxU4zt65+ceNiVDgQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.10.6", + "chromium-bidi": "7.2.0", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1475386", + "puppeteer-core": "24.16.0", + "typed-query-selector": "^2.12.0" + }, + "bin": { + "puppeteer": "lib/cjs/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core": { + "version": "24.16.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.16.0.tgz", + "integrity": "sha512-tZ0tJiOYaDGTRzzr2giDpf8O/55JsoqkrafS1Xu4H6S8oP4eeL6RbZzY9OzjShSf5EQvx/zAc55QKpDqzXos/Q==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.10.6", + "chromium-bidi": "7.2.0", + "debug": "^4.4.1", + "devtools-protocol": "0.0.1475386", + "typed-query-selector": "^2.12.0", + "ws": "^8.18.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz", + "integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.46.2", + "@rollup/rollup-android-arm64": "4.46.2", + "@rollup/rollup-darwin-arm64": "4.46.2", + "@rollup/rollup-darwin-x64": "4.46.2", + "@rollup/rollup-freebsd-arm64": "4.46.2", + "@rollup/rollup-freebsd-x64": "4.46.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.46.2", + "@rollup/rollup-linux-arm-musleabihf": "4.46.2", + "@rollup/rollup-linux-arm64-gnu": "4.46.2", + "@rollup/rollup-linux-arm64-musl": "4.46.2", + "@rollup/rollup-linux-loongarch64-gnu": "4.46.2", + "@rollup/rollup-linux-ppc64-gnu": "4.46.2", + "@rollup/rollup-linux-riscv64-gnu": "4.46.2", + "@rollup/rollup-linux-riscv64-musl": "4.46.2", + "@rollup/rollup-linux-s390x-gnu": "4.46.2", + "@rollup/rollup-linux-x64-gnu": "4.46.2", + "@rollup/rollup-linux-x64-musl": "4.46.2", + "@rollup/rollup-win32-arm64-msvc": "4.46.2", + "@rollup/rollup-win32-ia32-msvc": "4.46.2", + "@rollup/rollup-win32-x64-msvc": "4.46.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.6.tgz", + "integrity": "sha512-pe4Y2yzru68lXCb38aAqRf5gvN8YdjP1lok5o0J7BOHljkyCGKVz7H3vpVIXKD27rj2giOJ7DwVyk/GWrPHDWA==", + "license": "MIT", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, + "node_modules/streamx": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz", + "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==", + "license": "MIT", + "dependencies": { + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", + "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar-fs": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.0.tgz", + "integrity": "sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typed-query-selector": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", + "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "license": "MIT", + "optional": true + }, + "node_modules/vite": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.1.tgz", + "integrity": "sha512-yJ+Mp7OyV+4S+afWo+QyoL9jFWD11QFH0i5i7JypnfTcA1rmgxCbiA8WwAICDEtZ1Z1hzrVhN8R8rGTqkTY8ZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.6", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.14" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-plugin-full-reload": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-full-reload/-/vite-plugin-full-reload-1.2.0.tgz", + "integrity": "sha512-kz18NW79x0IHbxRSHm0jttP4zoO9P9gXh+n6UTwlNKnviTTEpOlum6oS9SmecrTtSr+muHEn5TUuC75UovQzcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "picomatch": "^2.3.1" + } + }, + "node_modules/vite-plugin-full-reload/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json index a5707d8..8ccd7e1 100644 --- a/package.json +++ b/package.json @@ -13,5 +13,8 @@ "laravel-vite-plugin": "^2.0.0", "tailwindcss": "^4.0.0", "vite": "^7.0.4" + }, + "dependencies": { + "puppeteer": "^24.16.0" } } diff --git a/routes/api.php b/routes/api.php new file mode 100644 index 0000000..bbb9946 --- /dev/null +++ b/routes/api.php @@ -0,0 +1,49 @@ +name('api.crawl.serve'); +Route::get('shot/{uuid}.webp', [ShotController::class, 'serve'])->name('api.shot.serve'); + +// All other API routes require Sanctum authentication +Route::middleware(['auth:sanctum'])->group(function () { + + // Crawl endpoints + Route::prefix('crawl')->group(function () { + Route::post('/', [CrawlController::class, 'crawl'])->name('api.crawl.create'); + Route::get('/{uuid}', [CrawlController::class, 'status'])->name('api.crawl.status'); + Route::get('/', [CrawlController::class, 'index'])->name('api.crawl.index'); // Optional: list all crawl jobs + }); + + // Screenshot endpoints + Route::prefix('shot')->group(function () { + Route::post('/', [ShotController::class, 'shot'])->name('api.shot.create'); + Route::get('/{uuid}', [ShotController::class, 'status'])->name('api.shot.status'); + Route::get('/', [ShotController::class, 'index'])->name('api.shot.index'); // Optional: list all screenshot jobs + }); + +}); + +// Health check endpoint (no auth required) +Route::get('/health', function () { + return response()->json([ + 'status' => 'healthy', + 'timestamp' => now()->toISOString(), + 'service' => 'crawlshot' + ]); +})->name('api.health'); \ No newline at end of file diff --git a/src/CrawlshotClient.php b/src/CrawlshotClient.php new file mode 100644 index 0000000..a7c97d6 --- /dev/null +++ b/src/CrawlshotClient.php @@ -0,0 +1,111 @@ +baseUrl = rtrim($baseUrl, '/'); + $this->token = $token; + } + + /** + * POST /api/crawl - Create crawl job + */ + public function createCrawl(string $url, array $options = []): array + { + return $this->makeRequest('POST', '/api/crawl', array_merge(['url' => $url], $options)); + } + + /** + * GET /api/crawl/{uuid} - Get crawl status + */ + public function getCrawlStatus(string $uuid): array + { + return $this->makeRequest('GET', "/api/crawl/{$uuid}"); + } + + /** + * GET /api/crawl - List all crawl jobs + */ + public function listCrawls(): array + { + return $this->makeRequest('GET', '/api/crawl'); + } + + /** + * POST /api/shot - Create screenshot job + */ + public function createShot(string $url, array $options = []): array + { + return $this->makeRequest('POST', '/api/shot', array_merge(['url' => $url], $options)); + } + + /** + * GET /api/shot/{uuid} - Get screenshot status + */ + public function getShotStatus(string $uuid): array + { + return $this->makeRequest('GET', "/api/shot/{$uuid}"); + } + + /** + * GET /api/shot/{uuid}/download - Download screenshot file + */ + public function downloadShot(string $uuid): string + { + $response = Http::when($this->token, function ($http) { + return $http->withToken($this->token); + })->get($this->baseUrl . "/api/shot/{$uuid}/download"); + + if ($response->failed()) { + throw new \Exception("Failed to download screenshot: " . $response->body()); + } + + return $response->body(); + } + + /** + * GET /api/shot - List all screenshot jobs + */ + public function listShots(): array + { + return $this->makeRequest('GET', '/api/shot'); + } + + /** + * GET /api/health - Health check + */ + public function health(): array + { + return $this->makeRequest('GET', '/api/health', [], false); + } + + /** + * Make HTTP request to API + */ + protected function makeRequest(string $method, string $endpoint, array $data = [], bool $requiresAuth = true): array + { + $http = Http::when($requiresAuth && $this->token, function ($http) { + return $http->withToken($this->token); + }); + + $response = match (strtoupper($method)) { + 'GET' => $http->get($this->baseUrl . $endpoint), + 'POST' => $http->post($this->baseUrl . $endpoint, $data), + default => throw new \InvalidArgumentException("Unsupported HTTP method: {$method}") + }; + + if ($response->failed()) { + throw new \Exception("API request failed: " . $response->body()); + } + + return $response->json(); + } +} \ No newline at end of file diff --git a/src/CrawlshotServiceProvider.php b/src/CrawlshotServiceProvider.php new file mode 100644 index 0000000..1d8ba56 --- /dev/null +++ b/src/CrawlshotServiceProvider.php @@ -0,0 +1,39 @@ +mergeConfigFrom(__DIR__ . '/config/crawlshot.php', 'crawlshot'); + + // Register the client + $this->app->singleton('crawlshot', function ($app) { + return new CrawlshotClient( + $app['config']['crawlshot']['base_url'], + $app['config']['crawlshot']['token'] + ); + }); + + // Register facade alias + $this->app->alias('crawlshot', CrawlshotClient::class); + } + + /** + * Bootstrap any application services. + */ + public function boot(): void + { + // Publish configuration + $this->publishes([ + __DIR__ . '/config/crawlshot.php' => config_path('crawlshot.php'), + ], 'crawlshot-config'); + } +} \ No newline at end of file diff --git a/src/Facades/Crawlshot.php b/src/Facades/Crawlshot.php new file mode 100644 index 0000000..84d05d2 --- /dev/null +++ b/src/Facades/Crawlshot.php @@ -0,0 +1,28 @@ + env('CRAWLSHOT_BASE_URL', 'https://crawlshot.test'), + + /* + |-------------------------------------------------------------------------- + | Authentication Token + |-------------------------------------------------------------------------- + | + | Your Sanctum authentication token for the Crawlshot API. + | + */ + 'token' => env('CRAWLSHOT_TOKEN'), +]; \ No newline at end of file