From b3aea18d9cfe58c80f55811d0b16d568d08efaaf Mon Sep 17 00:00:00 2001 From: King Omar Date: Wed, 22 Jul 2026 01:52:31 +1000 Subject: [PATCH] Initial commit: RADICAL_SEARCH self-hosted web search engine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Crawler (Majestic Million → undici → cheerio → Meilisearch), Hono search API with P2P peer federation, Cloudflare Worker frontend, VM provisioning scripts. Adds README, .gitignore, and moves VM credentials out of deploy.sh into a git-ignored deploy.env. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 20 + README.md | 94 +++ api/package.json | 13 + api/src/index.js | 115 +++ crawler/package.json | 14 + crawler/src/fetcher.js | 47 ++ crawler/src/index.js | 66 ++ crawler/src/indexer.js | 40 + crawler/src/parser.js | 43 + crawler/src/tranco.js | 44 + frontend/package-lock.json | 1603 ++++++++++++++++++++++++++++++++++++ frontend/package.json | 16 + frontend/src/index.ts | 239 ++++++ frontend/wrangler.jsonc | 12 + scripts/deploy.env.example | 4 + scripts/deploy.sh | 27 + scripts/install-vm.sh | 74 ++ 17 files changed, 2471 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 api/package.json create mode 100644 api/src/index.js create mode 100644 crawler/package.json create mode 100644 crawler/src/fetcher.js create mode 100644 crawler/src/index.js create mode 100644 crawler/src/indexer.js create mode 100644 crawler/src/parser.js create mode 100644 crawler/src/tranco.js create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/src/index.ts create mode 100644 frontend/wrangler.jsonc create mode 100644 scripts/deploy.env.example create mode 100755 scripts/deploy.sh create mode 100755 scripts/install-vm.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..955031f --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# Dependencies +node_modules/ + +# Crawler data +crawler/domains.csv +crawler/*.csv + +# Wrangler / Cloudflare +.wrangler/ +dist/ + +# Local env / secrets (never commit VM passwords, keys) +.env +.env.local +deploy.env +*.local + +# Logs & OS +*.log +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..4fb38a8 --- /dev/null +++ b/README.md @@ -0,0 +1,94 @@ +# 🔍 RADICAL_SEARCH + +> **The open web — unfiltered.** A self-hosted, from-scratch web search engine. + +RADICAL_SEARCH crawls the most-linked domains on the web, indexes them in +[Meilisearch](https://www.meilisearch.com/), and serves results through a +Cloudflare Worker frontend at **[search.theradicalparty.com](https://search.theradicalparty.com)**. +The search API also supports optional **P2P federation** — nodes can register as +peers and fan queries out across a cluster. + +## Architecture + +``` + ┌─────────────────────────────┐ + browser ───▶ │ frontend/ (Cloudflare Worker) │ search.theradicalparty.com + │ SSR UI, dark/light, proxies /search │ + └───────────────┬─────────────┘ + │ fetch(API_URL) (HTTPS :443) + ▼ + ┌─────────────────────────────┐ + │ api/ (Hono, Node, on VM) │ search-api.theradicalparty.com + │ /search /peers /stats /health │ + └───────────────┬─────────────┘ + │ localhost:7700 + ▼ + ┌─────────────────────────────┐ + │ Meilisearch (index: pages) │ + └───────────────▲─────────────┘ + │ batched docs + ┌───────────────┴─────────────┐ + │ crawler/ (Node, on VM) │ Majestic Million → fetch → parse → index + └─────────────────────────────┘ +``` + +## Components + +| Dir | What it is | Runtime | +|-----|-----------|---------| +| `crawler/` | Downloads the [Majestic Million](https://majestic.com/reports/majestic-million) domain list, crawls each domain (undici, 8s timeout, HTML-only, 500 KB cap), parses with cheerio (title/description/body/outlinks), and batches docs into Meilisearch. | Node on the VM | +| `api/` | Hono search API. `/search` proxies to Meilisearch with highlighting and optional P2P peer fan-out. Also `/peers`, `/peers/register`, `/stats`, `/health`. | Node on the VM (`:PORT`) | +| `frontend/` | Cloudflare Worker. Server-rendered UI (Roboto Mono, dark/light theme), proxies `/search` to `API_URL`. | Cloudflare Workers | +| `scripts/` | `install-vm.sh` provisions Meilisearch + api + crawler as systemd services; `deploy.sh` syncs & runs it over SSH. | local → VM | + +## Local development + +```bash +# 1. Meilisearch (Docker) +docker run -it --rm -p 7700:7700 getmeili/meilisearch:latest --master-key masterKey + +# 2. API +cd api && npm install && MEILI_KEY=masterKey npm run dev # :3000 + +# 3. A small crawl +cd crawler && npm install && LIMIT=500 CONCURRENCY=8 npm start + +# 4. Frontend +cd frontend && npm install && npm run dev # wrangler dev +``` + +## Deploy + +**Frontend (Cloudflare Worker):** +```bash +cd frontend && npm run deploy +``` + +**API + crawler (VM):** copy `scripts/deploy.env.example` → `scripts/deploy.env`, +fill in the VM host/user/password (this file is git-ignored), then: +```bash +cd scripts && ./deploy.sh +``` + +### API exposure + +The frontend Worker reaches the API over **HTTPS on port 443** — a deployed +Cloudflare Worker's `fetch()` cannot reliably use non-standard ports like 3000. +The API is fronted at `search-api.theradicalparty.com` (see +[`docs/DEPLOY.md`](docs/DEPLOY.md) for the port/DNS setup). + +## Configuration + +| Var | Component | Default | Notes | +|-----|-----------|---------|-------| +| `MEILI_URL` | api, crawler | `http://localhost:7700` | Meilisearch endpoint | +| `MEILI_KEY` | api, crawler | `masterKey` | Meilisearch master key (VM-local only) | +| `PORT` | api | `3000` | API listen port | +| `NODE_ID` | api | `main` | P2P node identity | +| `CONCURRENCY` | crawler | `8` | Parallel fetches | +| `LIMIT` | crawler | `100000` | Max domains to crawl | +| `API_URL` | frontend | — | Set in `frontend/wrangler.jsonc` `vars` | + +## License + +Private / unlicensed — © the maintainer. diff --git a/api/package.json b/api/package.json new file mode 100644 index 0000000..a0dbb0c --- /dev/null +++ b/api/package.json @@ -0,0 +1,13 @@ +{ + "name": "search-engine-api", + "version": "1.0.0", + "type": "module", + "scripts": { + "start": "node src/index.js", + "dev": "node --watch src/index.js" + }, + "dependencies": { + "hono": "^4.7.0", + "@hono/node-server": "^1.14.0" + } +} diff --git a/api/src/index.js b/api/src/index.js new file mode 100644 index 0000000..4ed8d88 --- /dev/null +++ b/api/src/index.js @@ -0,0 +1,115 @@ +import { Hono } from 'hono' +import { serve } from '@hono/node-server' + +const app = new Hono() +const PORT = parseInt(process.env.PORT || '3000') +const MEILI_URL = process.env.MEILI_URL || 'http://localhost:7700' +const MEILI_KEY = process.env.MEILI_KEY || 'masterKey' +const NODE_ID = process.env.NODE_ID || 'main' + +// In-memory peer registry { id, url, lastSeen } +const peers = new Map() + +async function meiliSearch(q, offset = 0, limit = 10) { + const res = await fetch(`${MEILI_URL}/indexes/pages/search`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${MEILI_KEY}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + q, + limit, + offset, + attributesToHighlight: ['title', 'description'], + highlightPreTag: '', + highlightPostTag: '', + attributesToRetrieve: ['url', 'domain', 'title', 'description', 'crawledAt'], + }), + }) + return res.json() +} + +async function queryPeer(peer, q) { + try { + const res = await fetch(`${peer.url}/search?q=${encodeURIComponent(q)}&local=1`, { + signal: AbortSignal.timeout(3000), + headers: { 'X-Peer-Id': NODE_ID }, + }) + const data = await res.json() + return data.hits ?? [] + } catch { + return [] + } +} + +// Search endpoint — fans out to peers when not local +app.get('/search', async (c) => { + const q = c.req.query('q')?.trim() + const page = Math.max(0, parseInt(c.req.query('page') ?? '0')) + const local = c.req.query('local') === '1' + + if (!q) return c.json({ error: 'missing q' }, 400) + + const offset = page * 10 + const local$ = meiliSearch(q, offset) + const peer$ = local ? [] : [...peers.values()].map(p => queryPeer(p, q)) + + const [localResult, ...peerResults] = await Promise.all([local$, ...peer$]) + + // Deduplicate and merge peer hits + const seen = new Set(localResult.hits?.map(h => h.url) ?? []) + const peerHits = peerResults + .flat() + .filter(h => !seen.has(h.url)) + .slice(0, 5) + + const hits = [...(localResult.hits ?? []), ...peerHits] + + return c.json({ + hits, + total: (localResult.estimatedTotalHits ?? 0) + peerHits.length, + page, + query: q, + nodeId: NODE_ID, + peers: [...peers.keys()], + }) +}) + +// P2P: register as peer +app.post('/peers/register', async (c) => { + const { id, url } = await c.req.json() + if (!id || !url) return c.json({ error: 'id and url required' }, 400) + peers.set(id, { id, url, lastSeen: Date.now() }) + return c.json({ ok: true, peerId: NODE_ID, peers: [...peers.keys()] }) +}) + +// P2P: list peers +app.get('/peers', (c) => { + return c.json({ nodeId: NODE_ID, peers: [...peers.values()] }) +}) + +// Health +app.get('/health', async (c) => { + try { + const res = await fetch(`${MEILI_URL}/health`, { + headers: { 'Authorization': `Bearer ${MEILI_KEY}` } + }) + const meili = await res.json() + return c.json({ ok: true, meili, nodeId: NODE_ID }) + } catch (e) { + return c.json({ ok: false, error: e.message }, 503) + } +}) + +// Stats +app.get('/stats', async (c) => { + const res = await fetch(`${MEILI_URL}/indexes/pages/stats`, { + headers: { 'Authorization': `Bearer ${MEILI_KEY}` } + }) + const stats = await res.json() + return c.json({ ...stats, peers: peers.size, nodeId: NODE_ID }) +}) + +serve({ fetch: app.fetch, port: PORT }) +console.log(`Search API running on :${PORT} [node=${NODE_ID}]`) diff --git a/crawler/package.json b/crawler/package.json new file mode 100644 index 0000000..e3f771e --- /dev/null +++ b/crawler/package.json @@ -0,0 +1,14 @@ +{ + "name": "search-engine-crawler", + "version": "1.0.0", + "type": "module", + "scripts": { + "start": "node src/index.js", + "tranco": "node src/tranco.js" + }, + "dependencies": { + "cheerio": "^1.0.0", + "p-limit": "^6.2.0", + "undici": "^7.0.0" + } +} diff --git a/crawler/src/fetcher.js b/crawler/src/fetcher.js new file mode 100644 index 0000000..49d12cd --- /dev/null +++ b/crawler/src/fetcher.js @@ -0,0 +1,47 @@ +import { fetch } from 'undici' + +const TIMEOUT_MS = 8000 +const MAX_BODY_BYTES = 500_000 + +const HEADERS = { + 'User-Agent': 'SearchBot/1.0 (compatible; +https://search.theradicalparty.com/bot)', + 'Accept': 'text/html,application/xhtml+xml', + 'Accept-Language': 'en', +} + +export async function fetchPage(domain) { + const urls = [`https://${domain}`, `http://${domain}`] + for (const url of urls) { + try { + const controller = new AbortController() + const timer = setTimeout(() => controller.abort(), TIMEOUT_MS) + const res = await fetch(url, { + headers: HEADERS, + signal: controller.signal, + maxRedirections: 3, + }) + clearTimeout(timer) + + if (!res.ok) continue + const contentType = res.headers.get('content-type') ?? '' + if (!contentType.includes('html')) continue + + // Read up to MAX_BODY_BYTES + const reader = res.body.getReader() + const chunks = [] + let total = 0 + while (true) { + const { done, value } = await reader.read() + if (done) break + chunks.push(value) + total += value.length + if (total >= MAX_BODY_BYTES) { reader.cancel(); break } + } + const html = Buffer.concat(chunks).toString('utf8') + return { url, html, status: res.status } + } catch { + // try next URL + } + } + return null +} diff --git a/crawler/src/index.js b/crawler/src/index.js new file mode 100644 index 0000000..0d93274 --- /dev/null +++ b/crawler/src/index.js @@ -0,0 +1,66 @@ +import pLimit from 'p-limit' +import { downloadTranco, readDomains } from './tranco.js' +import { fetchPage } from './fetcher.js' +import { parsePage } from './parser.js' +import { setupIndex, indexDoc, flush } from './indexer.js' + +const CONCURRENCY = parseInt(process.env.CONCURRENCY || '8') +const LIMIT = parseInt(process.env.LIMIT || '100000') + +let crawled = 0 +let failed = 0 +let skipped = 0 + +function log() { + if (crawled % 50 === 0) { + process.stdout.write(`\r[crawled=${crawled} failed=${failed} skipped=${skipped}]`) + } +} + +async function crawlDomain({ rank, domain }) { + const result = await fetchPage(domain) + if (!result) { failed++; log(); return } + + const doc = parsePage(result.url, result.html) + if (!doc.title && !doc.description) { skipped++; log(); return } + + doc.rank = rank + await indexDoc(doc) + crawled++ + log() +} + +// Process domains in a bounded sliding window instead of creating all promises upfront +async function main() { + await downloadTranco() + await setupIndex() + + console.log(`Starting crawl: concurrency=${CONCURRENCY} limit=${LIMIT}`) + + const queue = [] + let active = 0 + + async function runNext(entry) { + active++ + try { await crawlDomain(entry) } catch {} + active-- + } + + for (const entry of readDomains(LIMIT)) { + while (active >= CONCURRENCY) { + await new Promise(r => setTimeout(r, 10)) + } + queue.push(runNext(entry)) + // Periodically drain settled promises to free memory + if (queue.length >= 500) { + await Promise.allSettled(queue.splice(0, 200)) + } + } + + await Promise.allSettled(queue) + await flush() + + console.log(`\nDone. crawled=${crawled} failed=${failed} skipped=${skipped}`) +} + +main().catch(console.error) diff --git a/crawler/src/indexer.js b/crawler/src/indexer.js new file mode 100644 index 0000000..8b21f1e --- /dev/null +++ b/crawler/src/indexer.js @@ -0,0 +1,40 @@ +const MEILI_URL = process.env.MEILI_URL || 'http://localhost:7700' +const MEILI_KEY = process.env.MEILI_KEY || 'masterKey' +const INDEX = 'pages' +const BATCH_SIZE = 100 + +let buffer = [] + +async function meili(method, path, body) { + const res = await fetch(`${MEILI_URL}${path}`, { + method, + headers: { + 'Authorization': `Bearer ${MEILI_KEY}`, + 'Content-Type': 'application/json', + }, + body: body ? JSON.stringify(body) : undefined, + }) + return res.json() +} + +export async function setupIndex() { + await meili('POST', '/indexes', { uid: INDEX, primaryKey: 'id' }) + await meili('PATCH', `/indexes/${INDEX}/settings`, { + searchableAttributes: ['title', 'description', 'domain', 'body'], + displayedAttributes: ['id', 'url', 'domain', 'title', 'description', 'crawledAt'], + rankingRules: ['words', 'typo', 'proximity', 'attribute', 'sort', 'exactness'], + filterableAttributes: ['lang', 'domain'], + }) + console.log('Index ready') +} + +export async function indexDoc(doc) { + buffer.push(doc) + if (buffer.length >= BATCH_SIZE) await flush() +} + +export async function flush() { + if (buffer.length === 0) return + const docs = buffer.splice(0) + await meili('POST', `/indexes/${INDEX}/documents`, docs) +} diff --git a/crawler/src/parser.js b/crawler/src/parser.js new file mode 100644 index 0000000..341aaa5 --- /dev/null +++ b/crawler/src/parser.js @@ -0,0 +1,43 @@ +import { load } from 'cheerio' + +export function parsePage(url, html) { + const $ = load(html) + + // Remove noise + $('script, style, noscript, nav, footer, header, aside, [aria-hidden=true]').remove() + + const title = ($('title').text() || $('h1').first().text()).trim().slice(0, 200) + + const description = ( + $('meta[name=description]').attr('content') || + $('meta[property="og:description"]').attr('content') || + $('p').first().text() + ).trim().slice(0, 500) + + const body = $('body').text().replace(/\s+/g, ' ').trim().slice(0, 5000) + + const lang = $('html').attr('lang') || 'en' + + const outlinks = [] + $('a[href]').each((_, el) => { + try { + const href = new URL($(el).attr('href'), url).href + if (href.startsWith('http')) outlinks.push(href) + } catch {} + }) + + const domain = new URL(url).hostname.replace(/^www\./, '') + const id = domain.replace(/[^a-zA-Z0-9_-]/g, '_') + + return { + id, + url, + domain, + title: title || domain, + description, + body, + lang, + outlinks: [...new Set(outlinks)].slice(0, 50), + crawledAt: new Date().toISOString(), + } +} diff --git a/crawler/src/tranco.js b/crawler/src/tranco.js new file mode 100644 index 0000000..7a4e79a --- /dev/null +++ b/crawler/src/tranco.js @@ -0,0 +1,44 @@ +import { createWriteStream, existsSync, readFileSync } from 'fs' +import { pipeline } from 'stream/promises' + +// Majestic Million - stable URL, updated daily, rank,domain format +const LIST_URL = 'https://downloads.majestic.com/majestic_million.csv' +const LIST_FILE = './domains.csv' + +export async function downloadTranco() { + if (existsSync(LIST_FILE)) { + console.log('Domain list already downloaded') + return + } + console.log('Downloading Majestic Million domain list...') + const res = await fetch(LIST_URL) + if (!res.ok) throw new Error(`Failed: ${res.status}`) + await pipeline(res.body, createWriteStream(LIST_FILE)) + console.log('Download complete') +} + +export function* readDomains(limit = Infinity) { + const csv = readFileSync(LIST_FILE, 'utf8') + let count = 0 + let header = true + for (const line of csv.split('\n')) { + if (!line.trim()) continue + // Skip header row + if (header) { header = false; continue } + const cols = line.split(',') + const rank = parseInt(cols[0]) + const domain = cols[2]?.trim() // Majestic: GlobalRank,TldRank,Domain,... + if (!domain || isNaN(rank)) continue + yield { rank, domain } + if (++count >= limit) break + } +} + +if (process.argv[1].endsWith('tranco.js')) { + await downloadTranco() + let i = 0 + for (const entry of readDomains(10)) { + console.log(entry) + i++ + } +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..bb19d66 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,1603 @@ +{ + "name": "search-engine-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "search-engine-frontend", + "version": "1.0.0", + "dependencies": { + "hono": "^4.7.0" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.0.0", + "wrangler": "^3.0.0" + } + }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.3.4.tgz", + "integrity": "sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "mime": "^3.0.0" + }, + "engines": { + "node": ">=16.13" + } + }, + "node_modules/@cloudflare/unenv-preset": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.0.2.tgz", + "integrity": "sha512-nyzYnlZjjV5xT3LizahG1Iu6mnrCaxglJ04rZLpDwlDVDZ7v46lNsfxhV3A/xtfgQuSHmLnc6SVI+KwBpc3Lwg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.14", + "workerd": "^1.20250124.0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250718.0.tgz", + "integrity": "sha512-FHf4t7zbVN8yyXgQ/r/GqLPaYZSGUVzeR7RnL28Mwj2djyw2ZergvytVc7fdGcczl6PQh+VKGfZCfUqpJlbi9g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250718.0.tgz", + "integrity": "sha512-fUiyUJYyqqp4NqJ0YgGtp4WJh/II/YZsUnEb6vVy5Oeas8lUOxnN+ZOJ8N/6/5LQCVAtYCChRiIrBbfhTn5Z8Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250718.0.tgz", + "integrity": "sha512-5+eb3rtJMiEwp08Kryqzzu8d1rUcK+gdE442auo5eniMpT170Dz0QxBrqkg2Z48SFUPYbj+6uknuA5tzdRSUSg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250718.0.tgz", + "integrity": "sha512-Aa2M/DVBEBQDdATMbn217zCSFKE+ud/teS+fFS+OQqKABLn0azO2qq6ANAHYOIE6Q3Sq4CxDIQr8lGdaJHwUog==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250718.0.tgz", + "integrity": "sha512-dY16RXKffmugnc67LTbyjdDHZn5NoTF1yHEf2fN4+OaOnoGSp3N1x77QubTDwqZ9zECWxgQfDLjddcH8dWeFhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workers-types": { + "version": "4.20260702.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260702.1.tgz", + "integrity": "sha512-mOhf5TUEB1m2vPrxtqoIGfz0fUC9xyxRDx5gWHy5s+OCo6dcV+g7wI1R7gYCMFohhqF/2y2xeKVwMwCJjfn/WA==", + "dev": true, + "license": "MIT OR Apache-2.0" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild-plugins/node-globals-polyfill": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-globals-polyfill/-/node-globals-polyfill-0.2.3.tgz", + "integrity": "sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "esbuild": "*" + } + }, + "node_modules/@esbuild-plugins/node-modules-polyfill": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-modules-polyfill/-/node-modules-polyfill-0.2.2.tgz", + "integrity": "sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA==", + "dev": true, + "license": "ISC", + "dependencies": { + "escape-string-regexp": "^4.0.0", + "rollup-plugin-node-polyfills": "^0.2.1" + }, + "peerDependencies": { + "esbuild": "*" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", + "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", + "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", + "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", + "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", + "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", + "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", + "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", + "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", + "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", + "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", + "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", + "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", + "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", + "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", + "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", + "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", + "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", + "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", + "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", + "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", + "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", + "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/as-table": { + "version": "1.0.55", + "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz", + "integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "printable-characters": "^1.0.42" + } + }, + "node_modules/blake3-wasm": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", + "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "dev": true, + "license": "MIT" + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz", + "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==", + "dev": true, + "license": "MIT" + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/esbuild": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", + "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.17.19", + "@esbuild/android-arm64": "0.17.19", + "@esbuild/android-x64": "0.17.19", + "@esbuild/darwin-arm64": "0.17.19", + "@esbuild/darwin-x64": "0.17.19", + "@esbuild/freebsd-arm64": "0.17.19", + "@esbuild/freebsd-x64": "0.17.19", + "@esbuild/linux-arm": "0.17.19", + "@esbuild/linux-arm64": "0.17.19", + "@esbuild/linux-ia32": "0.17.19", + "@esbuild/linux-loong64": "0.17.19", + "@esbuild/linux-mips64el": "0.17.19", + "@esbuild/linux-ppc64": "0.17.19", + "@esbuild/linux-riscv64": "0.17.19", + "@esbuild/linux-s390x": "0.17.19", + "@esbuild/linux-x64": "0.17.19", + "@esbuild/netbsd-x64": "0.17.19", + "@esbuild/openbsd-x64": "0.17.19", + "@esbuild/sunos-x64": "0.17.19", + "@esbuild/win32-arm64": "0.17.19", + "@esbuild/win32-ia32": "0.17.19", + "@esbuild/win32-x64": "0.17.19" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/exit-hook": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz", + "integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/exsolve": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.0.tgz", + "integrity": "sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-source": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz", + "integrity": "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "data-uri-to-buffer": "^2.0.0", + "source-map": "^0.6.1" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/hono": { + "version": "4.12.30", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.30.tgz", + "integrity": "sha512-emn+JoJjrN9YTpRDS5it/UI2SO9BAE37T6I3d963RxcZ81G9A4pr2SZTEiiaiKbzx+NKRg5BZ89fCL7gCJCUog==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "sourcemap-codec": "^1.4.8" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/miniflare": { + "version": "3.20250718.3", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20250718.3.tgz", + "integrity": "sha512-JuPrDJhwLrNLEJiNLWO7ZzJrv/Vv9kZuwMYCfv0LskQDM6Eonw4OvywO3CH/wCGjgHzha/qyjUh8JQ068TjDgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "acorn": "8.14.0", + "acorn-walk": "8.3.2", + "exit-hook": "2.2.1", + "glob-to-regexp": "0.4.1", + "stoppable": "1.1.0", + "undici": "^5.28.5", + "workerd": "1.20250718.0", + "ws": "8.18.0", + "youch": "3.3.4", + "zod": "3.22.3" + }, + "bin": { + "miniflare": "bootstrap.js" + }, + "engines": { + "node": ">=16.13" + } + }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "dev": true, + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/printable-characters": { + "version": "1.0.42", + "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", + "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/rollup-plugin-inject": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rollup-plugin-inject/-/rollup-plugin-inject-3.0.2.tgz", + "integrity": "sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==", + "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject.", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^0.6.1", + "magic-string": "^0.25.3", + "rollup-pluginutils": "^2.8.1" + } + }, + "node_modules/rollup-plugin-node-polyfills": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-node-polyfills/-/rollup-plugin-node-polyfills-0.2.1.tgz", + "integrity": "sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==", + "dev": true, + "license": "MIT", + "dependencies": { + "rollup-plugin-inject": "^3.0.0" + } + }, + "node_modules/rollup-pluginutils": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", + "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^0.6.1" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead", + "dev": true, + "license": "MIT" + }, + "node_modules/stacktracey": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.2.0.tgz", + "integrity": "sha512-ETyQEz+CzXiLjEbyJqpbp+/T79RQD/6wqFucRBIlVNZfYq2Ay7wbretD4cxpbymZlaPWx58aIhPEY1Cr8DlVvg==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "as-table": "^1.0.36", + "get-source": "^2.0.12" + } + }, + "node_modules/stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4", + "npm": ">=6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/unenv": { + "version": "2.0.0-rc.14", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.14.tgz", + "integrity": "sha512-od496pShMen7nOy5VmVJCnq8rptd45vh6Nx/r2iPbrba6pa6p+tS2ywuIHRZ/OBvSbQZB0kWvpO9XBNVFXHD3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "exsolve": "^1.0.1", + "ohash": "^2.0.10", + "pathe": "^2.0.3", + "ufo": "^1.5.4" + } + }, + "node_modules/workerd": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20250718.0.tgz", + "integrity": "sha512-kqkIJP/eOfDlUyBzU7joBg+tl8aB25gEAGqDap+nFWb+WHhnooxjGHgxPBy3ipw2hnShPFNOQt5lFRxbwALirg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20250718.0", + "@cloudflare/workerd-darwin-arm64": "1.20250718.0", + "@cloudflare/workerd-linux-64": "1.20250718.0", + "@cloudflare/workerd-linux-arm64": "1.20250718.0", + "@cloudflare/workerd-windows-64": "1.20250718.0" + } + }, + "node_modules/wrangler": { + "version": "3.114.17", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.114.17.tgz", + "integrity": "sha512-tAvf7ly+tB+zwwrmjsCyJ2pJnnc7SZhbnNwXbH+OIdVas3zTSmjcZOjmLKcGGptssAA3RyTKhcF9BvKZzMUycA==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.3.4", + "@cloudflare/unenv-preset": "2.0.2", + "@esbuild-plugins/node-globals-polyfill": "0.2.3", + "@esbuild-plugins/node-modules-polyfill": "0.2.2", + "blake3-wasm": "2.1.5", + "esbuild": "0.17.19", + "miniflare": "3.20250718.3", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.14", + "workerd": "1.20250718.0" + }, + "bin": { + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=16.17.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2", + "sharp": "^0.33.5" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^4.20250408.0" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/youch": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/youch/-/youch-3.3.4.tgz", + "integrity": "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie": "^0.7.1", + "mustache": "^4.2.0", + "stacktracey": "^2.1.8" + } + }, + "node_modules/zod": { + "version": "3.22.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz", + "integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..14b63c3 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,16 @@ +{ + "name": "search-engine-frontend", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "wrangler dev", + "deploy": "wrangler deploy" + }, + "dependencies": { + "hono": "^4.7.0" + }, + "devDependencies": { + "wrangler": "^3.0.0", + "@cloudflare/workers-types": "^4.0.0" + } +} diff --git a/frontend/src/index.ts b/frontend/src/index.ts new file mode 100644 index 0000000..aebbe8c --- /dev/null +++ b/frontend/src/index.ts @@ -0,0 +1,239 @@ +const CSS = ` + @import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400;500;700&display=swap'); + *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + + /* Dark (default) */ + :root { + --bg: #0a0a0a; --surface: #111; --border: #222; + --text: #e8e8e8; --muted: #666; --accent: #ff0099; + --mark: rgba(255,0,153,0.2); --title: #fff; + --footer-text: #333; --footer-link: #444; --snippet: #888; --peers: #444; + } + + /* Light */ + html.light { + --bg: #f4f4f4; --surface: #fff; --border: #ddd; + --text: #111; --muted: #888; --accent: #cc007a; + --mark: rgba(204,0,122,0.15); --title: #0a0a0a; + --footer-text: #aaa; --footer-link: #999; --snippet: #555; --peers: #bbb; + } + + body { background: var(--bg); color: var(--text); font-family: 'Roboto Mono', monospace; min-height: 100vh; transition: background .2s, color .2s; } + a { color: var(--accent); text-decoration: none; } + a:hover { filter: brightness(1.15); } + mark { background: var(--mark); color: inherit; padding: 0 2px; } + + /* Theme toggle */ + .theme-toggle { + position: fixed; top: 16px; right: 16px; + background: none; border: 1px solid var(--border); + font-size: 16px; padding: 6px 10px; cursor: pointer; line-height: 1; + transition: border-color .15s; z-index: 100; + } + .theme-toggle:hover { border-color: var(--accent); } + + /* Home page */ + .home { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; gap: 40px; } + .home-brand { text-align: center; } + .logo { font-size: 48px; font-weight: 700; letter-spacing: -1px; color: var(--title); line-height: 1; } + .logo span { color: var(--accent); } + .tagline { color: var(--muted); font-size: 12px; letter-spacing: 3px; text-transform: uppercase; margin-top: 10px; } + + /* Search bar */ + .search-form { display: flex; flex-direction: column; gap: 12px; width: 100%; max-width: 580px; padding: 0 16px; } + .search-input { + width: 100%; background: var(--surface); border: 1px solid var(--border); + border-radius: 0; padding: 14px 18px; font-size: 15px; color: var(--text); + font-family: 'Roboto Mono', monospace; outline: none; transition: border-color .15s, background .2s; + } + .search-input:focus { border-color: var(--accent); } + .search-input::placeholder { color: var(--muted); } + .search-btn { + background: var(--accent); border: none; padding: 12px; + color: #fff; font-size: 13px; font-weight: 700; font-family: 'Roboto Mono', monospace; + letter-spacing: 2px; text-transform: uppercase; cursor: pointer; transition: filter .15s; + } + .search-btn:hover { filter: brightness(1.15); } + + /* Results page */ + .results-page { max-width: 720px; margin: 0 auto; padding: 0 16px 80px; } + .results-header { + display: flex; align-items: center; gap: 16px; + padding: 20px 0; border-bottom: 1px solid var(--border); margin-bottom: 28px; + } + .results-logo { font-size: 20px; font-weight: 700; white-space: nowrap; color: var(--title); } + .results-logo span { color: var(--accent); } + .results-form { display: flex; gap: 8px; flex: 1; } + .results-input { + flex: 1; background: var(--surface); border: 1px solid var(--border); + border-radius: 0; padding: 9px 14px; font-size: 14px; color: var(--text); + font-family: 'Roboto Mono', monospace; outline: none; transition: border-color .15s, background .2s; + } + .results-input:focus { border-color: var(--accent); } + .results-btn { + background: var(--accent); border: none; padding: 9px 18px; + color: #fff; font-size: 12px; font-weight: 700; font-family: 'Roboto Mono', monospace; + letter-spacing: 1px; text-transform: uppercase; cursor: pointer; white-space: nowrap; + } + .results-btn:hover { filter: brightness(1.15); } + .meta { color: var(--muted); font-size: 12px; margin-bottom: 28px; letter-spacing: 0.5px; } + .result { margin-bottom: 32px; border-left: 2px solid transparent; padding-left: 16px; transition: border-color .15s; } + .result:hover { border-left-color: var(--accent); } + .result-url { font-size: 11px; color: var(--muted); margin-bottom: 5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; letter-spacing: 0.5px; } + .result-title { font-size: 17px; font-weight: 500; line-height: 1.4; margin-bottom: 6px; } + .result-title a { color: var(--title); } + .result-title a:hover { color: var(--accent); } + .result-snippet { font-size: 13px; color: var(--snippet); line-height: 1.7; } + .pagination { display: flex; gap: 8px; margin-top: 40px; } + .page-btn { + background: var(--surface); border: 1px solid var(--border); + padding: 8px 16px; color: var(--muted); font-size: 12px; font-family: 'Roboto Mono', monospace; + letter-spacing: 1px; text-transform: uppercase; text-decoration: none; display: inline-block; + transition: border-color .15s, color .15s; + } + .page-btn:hover { border-color: var(--accent); color: var(--accent); } + .no-results { text-align: center; padding: 80px 20px; color: var(--muted); font-size: 13px; line-height: 2; } + .peers { color: var(--peers); margin-left: 8px; } + .footer { margin-top: 60px; padding-top: 20px; border-top: 1px solid var(--border); font-size: 11px; color: var(--footer-text); letter-spacing: 1px; } + .footer a { color: var(--footer-link); } + .footer a:hover { color: var(--accent); } +` + +const THEME_JS = ` + (function() { + var t = localStorage.getItem('theme'); + if (t === 'light') document.documentElement.classList.add('light'); + })(); + document.addEventListener('DOMContentLoaded', function() { + var btn = document.getElementById('theme-toggle'); + function update() { + var light = document.documentElement.classList.contains('light'); + btn.textContent = light ? '☀️' : '🌙'; + } + update(); + btn.addEventListener('click', function() { + document.documentElement.classList.toggle('light'); + localStorage.setItem('theme', document.documentElement.classList.contains('light') ? 'light' : 'dark'); + update(); + }); + }); +` + +function layout(title: string, body: string) { + return ` + + ${title} + + + + + + + ${body} + ` +} + +function homePage(q = '') { + return layout('Radical Search', ` +
+
+ +
The open web — unfiltered
+
+
+ + +
+
+ `) +} + +function resultsPage(q: string, data: any, page: number) { + const hits = data.hits ?? [] + const total = data.total ?? 0 + const peers = data.peers ?? [] + + const results = hits.length + ? hits.map((h: any) => { + const title = h._formatted?.title || h.title || h.domain + const snippet = h._formatted?.description || h.description || '' + return ` +
+
${esc(h.url)}
+ + ${snippet ? `
${snippet}
` : ''} +
` + }).join('') + : `
no results for “${esc(q)}
the index is still growing — try again soon
` + + const prevLink = page > 0 ? `← prev` : '' + const nextLink = hits.length === 10 ? `next →` : '' + + return layout(`${esc(q)} — Radical Search`, ` +
+
+ +
+ + +
+
+
+ ~${total.toLocaleString()} results + ${peers.length ? `// ${peers.length} peer${peers.length !== 1 ? 's' : ''} federated` : ''} +
+ ${results} + ${hits.length ? `` : ''} + +
+ `) +} + +function esc(s: string) { + return String(s ?? '').replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"') +} + +export default { + async fetch(req: Request, env: { API_URL: string }) { + const url = new URL(req.url) + + if (url.pathname === '/') { + return new Response(homePage(), { headers: { 'Content-Type': 'text/html; charset=utf-8' } }) + } + + if (url.pathname === '/search') { + const q = url.searchParams.get('q')?.trim() ?? '' + if (!q) return Response.redirect(url.origin, 302) + + const page = Math.max(0, parseInt(url.searchParams.get('page') ?? '0')) + + try { + const apiRes = await fetch( + `${env.API_URL}/search?q=${encodeURIComponent(q)}&page=${page}`, + { headers: { 'User-Agent': 'SearchFrontend/1.0' } } + ) + const data = await apiRes.json() as any + return new Response(resultsPage(q, data, page), { + headers: { 'Content-Type': 'text/html; charset=utf-8' } + }) + } catch (e: any) { + return new Response(resultsPage(q, { hits: [], total: 0 }, page), { + headers: { 'Content-Type': 'text/html; charset=utf-8' } + }) + } + } + + // Proxy API calls through (for stats, peers, etc.) + if (url.pathname.startsWith('/api/')) { + const apiPath = url.pathname.replace('/api', '') + const apiRes = await fetch(`${env.API_URL}${apiPath}${url.search}`) + return apiRes + } + + return new Response('Not found', { status: 404 }) + } +} diff --git a/frontend/wrangler.jsonc b/frontend/wrangler.jsonc new file mode 100644 index 0000000..81b1a40 --- /dev/null +++ b/frontend/wrangler.jsonc @@ -0,0 +1,12 @@ +{ + "name": "search-engine-frontend", + "main": "src/index.ts", + "compatibility_date": "2025-01-01", + "compatibility_flags": ["nodejs_compat"], + "routes": [ + { "pattern": "search.theradicalparty.com/*", "zone_name": "theradicalparty.com" } + ], + "vars": { + "API_URL": "http://search-api.theradicalparty.com:3000" + } +} diff --git a/scripts/deploy.env.example b/scripts/deploy.env.example new file mode 100644 index 0000000..0781787 --- /dev/null +++ b/scripts/deploy.env.example @@ -0,0 +1,4 @@ +# Copy to deploy.env (git-ignored) and fill in. Used by deploy.sh. +VM_HOST=your.vm.ip.address +VM_USER=root +VM_PASS=your-vm-password diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..fa3107e --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,27 @@ +#!/bin/bash +set -e + +# Load VM credentials from git-ignored deploy.env (copy deploy.env.example). +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +if [ -f "$SCRIPT_DIR/deploy.env" ]; then + set -a; . "$SCRIPT_DIR/deploy.env"; set +a +fi + +VM_HOST="${VM_HOST:?set VM_HOST in scripts/deploy.env}" +VM_USER="${VM_USER:?set VM_USER in scripts/deploy.env}" +VM_PASS="${VM_PASS:?set VM_PASS in scripts/deploy.env}" + +ssh_cmd() { sshpass -p "$VM_PASS" ssh -o StrictHostKeyChecking=no "$VM_USER@$VM_HOST" "$@"; } +scp_cmd() { sshpass -p "$VM_PASS" scp -o StrictHostKeyChecking=no -r "$@"; } + +echo "=== Syncing files to VM ===" +ssh_cmd "mkdir -p /tmp/search-api /tmp/search-crawler" +scp_cmd ../api/. "$VM_USER@$VM_HOST:/tmp/search-api/" +scp_cmd ../crawler/. "$VM_USER@$VM_HOST:/tmp/search-crawler/" +scp_cmd install-vm.sh "$VM_USER@$VM_HOST:/tmp/install-vm.sh" + +echo "=== Running install script ===" +ssh_cmd "bash /tmp/install-vm.sh" + +echo "=== Done ===" +ssh_cmd "systemctl status meilisearch --no-pager && systemctl status search-api --no-pager" diff --git a/scripts/install-vm.sh b/scripts/install-vm.sh new file mode 100755 index 0000000..321fe62 --- /dev/null +++ b/scripts/install-vm.sh @@ -0,0 +1,74 @@ +#!/bin/bash +set -e + +echo "=== Installing Meilisearch ===" +curl -L https://install.meilisearch.com | sh +mv ./meilisearch /usr/local/bin/meilisearch + +echo "=== Creating Meilisearch systemd service ===" +cat > /etc/systemd/system/meilisearch.service << 'EOF' +[Unit] +Description=Meilisearch Search Engine +After=network.target + +[Service] +User=root +ExecStart=/usr/local/bin/meilisearch --http-addr 0.0.0.0:7700 --master-key masterKey --db-path /var/lib/meilisearch/data +Restart=always +RestartSec=5 +Environment=MEILI_NO_ANALYTICS=true + +[Install] +WantedBy=multi-user.target +EOF + +mkdir -p /var/lib/meilisearch/data +systemctl daemon-reload +systemctl enable meilisearch +systemctl start meilisearch +echo "Meilisearch started" + +echo "=== Deploying Search API ===" +mkdir -p /opt/search-api +cp -r /tmp/search-api/. /opt/search-api/ +cd /opt/search-api +npm install --production + +cat > /etc/systemd/system/search-api.service << 'EOF' +[Unit] +Description=Search Engine API +After=network.target meilisearch.service + +[Service] +User=root +WorkingDirectory=/opt/search-api +ExecStart=/usr/bin/node src/index.js +Restart=always +RestartSec=5 +Environment=PORT=3000 +Environment=MEILI_URL=http://localhost:7700 +Environment=MEILI_KEY=masterKey +Environment=NODE_ID=main + +[Install] +WantedBy=multi-user.target +EOF + +systemctl daemon-reload +systemctl enable search-api +systemctl start search-api +echo "Search API started on :3000" + +echo "=== Deploying Crawler ===" +mkdir -p /opt/search-crawler +cp -r /tmp/search-crawler/. /opt/search-crawler/ +cd /opt/search-crawler +npm install + +echo "" +echo "Done! Services running:" +echo " Meilisearch: http://localhost:7700" +echo " Search API: http://localhost:3000" +echo "" +echo "To start crawling:" +echo " cd /opt/search-crawler && LIMIT=50000 CONCURRENCY=8 npm start"