diff --git a/.env.example b/.env.example index 7ec02d877..1ae55dcec 100644 --- a/.env.example +++ b/.env.example @@ -121,3 +121,17 @@ VITE_SENTRY_DSN= # - "flat" keeps pitch/rotation disabled (2D interaction) # - "3d" enables pitch/rotation interactions (default) VITE_MAP_INTERACTION_MODE=3d + + +# ------ Desktop Cloud Fallback (Vercel) ------ + +# Comma-separated list of valid API keys for desktop cloud fallback. +# Generate with: openssl rand -hex 24 | sed 's/^/wm_/' +WORLDMONITOR_VALID_KEYS= + + +# ------ Registration DB (Convex) ------ + +# Convex deployment URL for email registration storage. +# Set up at: https://dashboard.convex.dev/ +CONVEX_URL= diff --git a/api/[domain]/v1/[rpc].ts b/api/[domain]/v1/[rpc].ts index ca3d7b6b5..7ed18373e 100644 --- a/api/[domain]/v1/[rpc].ts +++ b/api/[domain]/v1/[rpc].ts @@ -9,6 +9,8 @@ export const config = { runtime: 'edge' }; import { createRouter } from '../../../server/router'; import { getCorsHeaders, isDisallowedOrigin } from '../../../server/cors'; +// @ts-expect-error — JS module, no declaration file +import { validateApiKey } from '../../_api-key.js'; import { mapErrorToResponse } from '../../../server/error-mapper'; import { createSeismologyServiceRoutes } from '../../../src/generated/server/worldmonitor/seismology/v1/service_server'; import { seismologyHandler } from '../../../server/worldmonitor/seismology/v1/handler'; @@ -92,6 +94,15 @@ export default async function handler(request: Request): Promise { return new Response(null, { status: 204, headers: corsHeaders }); } + // API key validation (origin-aware) + const keyCheck = validateApiKey(request); + if (keyCheck.required && !keyCheck.valid) { + return new Response(JSON.stringify({ error: keyCheck.error }), { + status: 401, + headers: { 'Content-Type': 'application/json', ...corsHeaders }, + }); + } + // Route matching const matchedHandler = router.match(request); if (!matchedHandler) { diff --git a/api/_api-key.js b/api/_api-key.js new file mode 100644 index 000000000..d7974bca7 --- /dev/null +++ b/api/_api-key.js @@ -0,0 +1,30 @@ +const DESKTOP_ORIGIN_PATTERNS = [ + /^https?:\/\/tauri\.localhost(:\d+)?$/, + /^https?:\/\/[a-z0-9-]+\.tauri\.localhost(:\d+)?$/i, + /^tauri:\/\/localhost$/, + /^asset:\/\/localhost$/, +]; + +function isDesktopOrigin(origin) { + return Boolean(origin) && DESKTOP_ORIGIN_PATTERNS.some(p => p.test(origin)); +} + +export function validateApiKey(req) { + const key = req.headers.get('X-WorldMonitor-Key'); + const origin = req.headers.get('Origin') || ''; + + if (isDesktopOrigin(origin)) { + if (!key) return { valid: false, required: true, error: 'API key required for desktop access' }; + const validKeys = (process.env.WORLDMONITOR_VALID_KEYS || '').split(',').filter(Boolean); + if (!validKeys.includes(key)) return { valid: false, required: true, error: 'Invalid API key' }; + return { valid: true, required: true }; + } + + if (key) { + const validKeys = (process.env.WORLDMONITOR_VALID_KEYS || '').split(',').filter(Boolean); + if (!validKeys.includes(key)) return { valid: false, required: true, error: 'Invalid API key' }; + return { valid: true, required: true }; + } + + return { valid: false, required: false }; +} diff --git a/api/_cors.js b/api/_cors.js index 3bccfe0c4..77d0637e9 100644 --- a/api/_cors.js +++ b/api/_cors.js @@ -19,7 +19,7 @@ export function getCorsHeaders(req, methods = 'GET, OPTIONS') { return { 'Access-Control-Allow-Origin': allowOrigin, 'Access-Control-Allow-Methods': methods, - 'Access-Control-Allow-Headers': 'Content-Type', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-WorldMonitor-Key', 'Access-Control-Max-Age': '86400', 'Vary': 'Origin', }; diff --git a/api/register-interest.js b/api/register-interest.js new file mode 100644 index 000000000..232320dbb --- /dev/null +++ b/api/register-interest.js @@ -0,0 +1,97 @@ +export const config = { runtime: 'edge' }; + +import { ConvexHttpClient } from 'convex/browser'; +import { getCorsHeaders, isDisallowedOrigin } from './_cors.js'; + +const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; +const MAX_EMAIL_LENGTH = 320; + +const rateLimitMap = new Map(); +const RATE_LIMIT = 5; +const RATE_WINDOW_MS = 60 * 60 * 1000; + +function isRateLimited(ip) { + const now = Date.now(); + const entry = rateLimitMap.get(ip); + if (!entry || now - entry.windowStart > RATE_WINDOW_MS) { + rateLimitMap.set(ip, { windowStart: now, count: 1 }); + return false; + } + entry.count += 1; + return entry.count > RATE_LIMIT; +} + +export default async function handler(req) { + if (isDisallowedOrigin(req)) { + return new Response(JSON.stringify({ error: 'Origin not allowed' }), { + status: 403, + headers: { 'Content-Type': 'application/json' }, + }); + } + + const cors = getCorsHeaders(req, 'POST, OPTIONS'); + + if (req.method === 'OPTIONS') { + return new Response(null, { status: 204, headers: cors }); + } + + if (req.method !== 'POST') { + return new Response(JSON.stringify({ error: 'Method not allowed' }), { + status: 405, + headers: { 'Content-Type': 'application/json', ...cors }, + }); + } + + const ip = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || 'unknown'; + if (isRateLimited(ip)) { + return new Response(JSON.stringify({ error: 'Too many requests' }), { + status: 429, + headers: { 'Content-Type': 'application/json', ...cors }, + }); + } + + let body; + try { + body = await req.json(); + } catch { + return new Response(JSON.stringify({ error: 'Invalid JSON' }), { + status: 400, + headers: { 'Content-Type': 'application/json', ...cors }, + }); + } + + const { email, source, appVersion } = body; + if (!email || typeof email !== 'string' || email.length > MAX_EMAIL_LENGTH || !EMAIL_RE.test(email)) { + return new Response(JSON.stringify({ error: 'Invalid email address' }), { + status: 400, + headers: { 'Content-Type': 'application/json', ...cors }, + }); + } + + const convexUrl = process.env.CONVEX_URL; + if (!convexUrl) { + return new Response(JSON.stringify({ error: 'Registration service unavailable' }), { + status: 503, + headers: { 'Content-Type': 'application/json', ...cors }, + }); + } + + try { + const client = new ConvexHttpClient(convexUrl); + const result = await client.mutation('registerInterest:register', { + email, + source: source || 'unknown', + appVersion: appVersion || 'unknown', + }); + return new Response(JSON.stringify(result), { + status: 200, + headers: { 'Content-Type': 'application/json', ...cors }, + }); + } catch (err) { + console.error('[register-interest] Convex error:', err); + return new Response(JSON.stringify({ error: 'Registration failed' }), { + status: 500, + headers: { 'Content-Type': 'application/json', ...cors }, + }); + } +} diff --git a/convex/registerInterest.ts b/convex/registerInterest.ts new file mode 100644 index 000000000..2367f7e20 --- /dev/null +++ b/convex/registerInterest.ts @@ -0,0 +1,32 @@ +import { mutation } from "./_generated/server"; +import { v } from "convex/values"; + +export const register = mutation({ + args: { + email: v.string(), + source: v.optional(v.string()), + appVersion: v.optional(v.string()), + }, + handler: async (ctx, args) => { + const normalizedEmail = args.email.trim().toLowerCase(); + + const existing = await ctx.db + .query("registrations") + .withIndex("by_normalized_email", (q) => q.eq("normalizedEmail", normalizedEmail)) + .first(); + + if (existing) { + return { status: "already_registered" as const }; + } + + await ctx.db.insert("registrations", { + email: args.email.trim(), + normalizedEmail, + registeredAt: Date.now(), + source: args.source ?? "unknown", + appVersion: args.appVersion ?? "unknown", + }); + + return { status: "registered" as const }; + }, +}); diff --git a/convex/schema.ts b/convex/schema.ts new file mode 100644 index 000000000..66a738ec3 --- /dev/null +++ b/convex/schema.ts @@ -0,0 +1,12 @@ +import { defineSchema, defineTable } from "convex/server"; +import { v } from "convex/values"; + +export default defineSchema({ + registrations: defineTable({ + email: v.string(), + normalizedEmail: v.string(), + registeredAt: v.number(), + source: v.optional(v.string()), + appVersion: v.optional(v.string()), + }).index("by_normalized_email", ["normalizedEmail"]), +}); diff --git a/convex/tsconfig.json b/convex/tsconfig.json new file mode 100644 index 000000000..217b4c2d8 --- /dev/null +++ b/convex/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["ES2021"], + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "skipLibCheck": true, + "allowJs": true, + "outDir": "./_generated" + }, + "include": ["./**/*.ts"], + "exclude": ["./_generated"] +} diff --git a/docs/API_KEY_DEPLOYMENT.md b/docs/API_KEY_DEPLOYMENT.md new file mode 100644 index 000000000..5260bf9a0 --- /dev/null +++ b/docs/API_KEY_DEPLOYMENT.md @@ -0,0 +1,140 @@ +# API Key Gating & Registration — Deployment Guide + +## Overview + +Desktop cloud fallback is gated on a `WORLDMONITOR_API_KEY`. Without a valid key, the desktop app operates local-only (sidecar). A registration form collects emails via Convex DB for future key distribution. + +## Architecture + +``` +Desktop App Cloud (Vercel) +┌──────────────────┐ ┌──────────────────────┐ +│ fetch('/api/...')│ │ api/[domain]/v1/[rpc]│ +│ │ │ │ │ │ +│ ┌──────▼───────┐ │ │ ┌──────▼───────┐ │ +│ │ sidecar try │ │ │ │ validateApiKey│ │ +│ │ (local-first)│ │ │ │ (origin-aware)│ │ +│ └──────┬───────┘ │ │ └──────┬───────┘ │ +│ fail │ │ │ 401 if invalid │ +│ ┌──────▼───────┐ │ fallback │ │ +│ │ WM key check │─┼──────────────►│ ┌──────────────┐ │ +│ │ (gate) │ │ +header │ │ route handler │ │ +│ └──────────────┘ │ │ └──────────────┘ │ +└──────────────────┘ └──────────────────────┘ +``` + +## Required Environment Variables + +### Vercel + +| Variable | Description | Example | +|----------|-------------|---------| +| `WORLDMONITOR_VALID_KEYS` | Comma-separated list of valid API keys | `wm_abc123def456,wm_xyz789` | +| `CONVEX_URL` | Convex deployment URL (from `npx convex deploy`) | `https://xyz-123.convex.cloud` | + +### Generating API keys + +Keys must be at least 16 characters (validated client-side). Recommended format: + +```bash +# Generate a key +openssl rand -hex 24 | sed 's/^/wm_/' +# Example output: wm_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6 +``` + +Add to `WORLDMONITOR_VALID_KEYS` in Vercel dashboard (comma-separated, no spaces). + +## Convex Setup + +### First-time deployment + +```bash +# 1. Install (already in package.json) +npm install + +# 2. Login to Convex +npx convex login + +# 3. Initialize project (creates .env.local with CONVEX_URL) +npx convex init + +# 4. Deploy schema and functions +npx convex deploy + +# 5. Copy the deployment URL to Vercel env vars +# The URL is printed by `npx convex deploy` and saved in .env.local +``` + +### Verify Convex deployment + +```bash +# Typecheck Convex functions +npx convex dev --typecheck + +# Open Convex dashboard to see registrations +npx convex dashboard +``` + +### Schema + +The `registrations` table stores: + +| Field | Type | Description | +|-------|------|-------------| +| `email` | string | Original email (for display) | +| `normalizedEmail` | string | Lowercased email (for dedup) | +| `registeredAt` | number | Unix timestamp | +| `source` | string? | Where the registration came from | +| `appVersion` | string? | Desktop app version | + +Indexed by `normalizedEmail` for duplicate detection. + +## Security Model + +### Client-side (desktop app) + +- `installRuntimeFetchPatch()` checks `WORLDMONITOR_API_KEY` before allowing cloud fallback +- Key must be present AND valid (min 16 chars) +- `secretsReady` promise ensures secrets are loaded before first fetch (2s timeout) +- Fail-closed: any error in key check blocks cloud fallback + +### Server-side (Vercel edge) + +- `api/_api-key.js` validates `X-WorldMonitor-Key` header on sebuf routes +- **Origin-aware**: desktop origins (`tauri.localhost`, `tauri://`, `asset://`) require a key +- Web origins (`worldmonitor.app`) pass through without a key +- Non-desktop origin with key header: key is still validated +- Invalid key returns `401 { error: "Invalid API key" }` + +### CORS + +`X-WorldMonitor-Key` is allowed in both `server/cors.ts` and `api/_cors.js`. + +## Verification Checklist + +After deployment: + +- [ ] Set `WORLDMONITOR_VALID_KEYS` in Vercel +- [ ] Set `CONVEX_URL` in Vercel +- [ ] Run `npx convex deploy` to push schema +- [ ] Desktop without key: cloud fallback blocked (console shows `cloud fallback blocked`) +- [ ] Desktop with invalid key: sebuf requests get `401` +- [ ] Desktop with valid key: cloud fallback works as before +- [ ] Web access: no key required, works normally +- [ ] Registration form: submit email, check Convex dashboard +- [ ] Duplicate email: shows "already registered" +- [ ] Existing settings tabs (LLMs, API Keys, Debug) unchanged + +## Files Reference + +| File | Role | +|------|------| +| `src/services/runtime.ts` | Client-side key gate + header attachment | +| `src/services/runtime-config.ts` | `WORLDMONITOR_API_KEY` type, validation, `secretsReady` | +| `api/_api-key.js` | Server-side key validation (origin-aware) | +| `api/[domain]/v1/[rpc].ts` | Sebuf gateway — calls `validateApiKey` | +| `api/register-interest.js` | Registration endpoint → Convex | +| `server/cors.ts` / `api/_cors.js` | CORS headers with `X-WorldMonitor-Key` | +| `src/components/WorldMonitorTab.ts` | Settings UI for key + registration | +| `convex/schema.ts` | Convex DB schema | +| `convex/registerInterest.ts` | Convex mutation | diff --git a/docs/DESKTOP_CONFIGURATION.md b/docs/DESKTOP_CONFIGURATION.md index 658a199f9..c284689b7 100644 --- a/docs/DESKTOP_CONFIGURATION.md +++ b/docs/DESKTOP_CONFIGURATION.md @@ -4,7 +4,7 @@ World Monitor desktop now uses a runtime configuration schema with per-feature t ## Secret keys -The desktop vault schema supports the following 17 keys used by services and relays: +The desktop vault schema (Rust `SUPPORTED_SECRET_KEYS`) supports the following 21 keys: - `GROQ_API_KEY` - `OPENROUTER_API_KEY` @@ -18,11 +18,17 @@ The desktop vault schema supports the following 17 keys used by services and rel - `ABUSEIPDB_API_KEY` - `NASA_FIRMS_API_KEY` - `WINGBITS_API_KEY` +- `WS_RELAY_URL` +- `VITE_WS_RELAY_URL` - `VITE_OPENSKY_RELAY_URL` - `OPENSKY_CLIENT_ID` - `OPENSKY_CLIENT_SECRET` - `AISSTREAM_API_KEY` -- `VITE_WS_RELAY_URL` +- `OLLAMA_API_URL` +- `OLLAMA_MODEL` +- `WORLDMONITOR_API_KEY` — gates cloud fallback access (min 16 chars) + +Note: `UC_DP_KEY` exists in the TypeScript `RuntimeSecretKey` union but is not in the desktop Rust keychain or sidecar. ## Feature schema @@ -51,3 +57,4 @@ If required secrets are missing/disabled: - NASA FIRMS: satellite fire detection returns empty state. - Wingbits: flight enrichment disabled, heuristic-only flight classification remains. - AIS / OpenSky relay: live tracking features are disabled cleanly. +- WorldMonitor API key: cloud fallback is blocked; desktop operates local-only. diff --git a/e2e/runtime-fetch.spec.ts b/e2e/runtime-fetch.spec.ts index 9b933f0c1..73514b200 100644 --- a/e2e/runtime-fetch.spec.ts +++ b/e2e/runtime-fetch.spec.ts @@ -73,6 +73,7 @@ test.describe('desktop runtime routing guardrails', () => { const result = await page.evaluate(async () => { const runtime = await import('/src/services/runtime.ts'); + const runtimeConfig = await import('/src/services/runtime-config.ts'); const globalWindow = window as unknown as Record; const originalFetch = window.fetch.bind(window); @@ -114,6 +115,9 @@ test.describe('desktop runtime routing guardrails', () => { globalWindow.__TAURI__ = { core: { invoke: () => Promise.resolve(null) } }; delete globalWindow.__wmFetchPatched; + // Set a valid WM API key so cloud fallback is allowed + await runtimeConfig.setSecretValue('WORLDMONITOR_API_KEY' as import('/src/services/runtime-config.ts').RuntimeSecretKey, 'wm_test_key_1234567890abcdef'); + try { runtime.installRuntimeFetchPatch(); @@ -138,6 +142,7 @@ test.describe('desktop runtime routing guardrails', () => { } else { globalWindow.__TAURI__ = previousTauri; } + await runtimeConfig.setSecretValue('WORLDMONITOR_API_KEY' as import('/src/services/runtime-config.ts').RuntimeSecretKey, ''); } }); @@ -716,6 +721,156 @@ test.describe('desktop runtime routing guardrails', () => { expect(result.hasIso3Field).toBe(false); }); + test('cloud fallback blocked without WorldMonitor API key', async ({ page }) => { + await page.goto('/tests/runtime-harness.html'); + + const result = await page.evaluate(async () => { + const runtime = await import('/src/services/runtime.ts'); + const globalWindow = window as unknown as Record; + const originalFetch = window.fetch.bind(window); + + const calls: string[] = []; + const responseJson = (body: unknown, status = 200) => + new Response(JSON.stringify(body), { + status, + headers: { 'content-type': 'application/json' }, + }); + + window.fetch = (async (input: RequestInfo | URL) => { + const url = + typeof input === 'string' + ? input + : input instanceof URL + ? input.toString() + : input.url; + + calls.push(url); + + if (url.includes('127.0.0.1:46123/api/fred-data')) { + throw new Error('ECONNREFUSED'); + } + if (url.includes('worldmonitor.app/api/fred-data')) { + return responseJson({ observations: [{ value: '999' }] }, 200); + } + return responseJson({ ok: true }, 200); + }) as typeof window.fetch; + + const previousTauri = globalWindow.__TAURI__; + globalWindow.__TAURI__ = { core: { invoke: () => Promise.resolve(null) } }; + delete globalWindow.__wmFetchPatched; + + try { + runtime.installRuntimeFetchPatch(); + + let fetchError: string | null = null; + try { + await window.fetch('/api/fred-data?series_id=CPIAUCSL'); + } catch (err) { + fetchError = err instanceof Error ? err.message : String(err); + } + + const cloudCalls = calls.filter(u => u.includes('worldmonitor.app')); + + return { + fetchError, + cloudCalls: cloudCalls.length, + localCalls: calls.filter(u => u.includes('127.0.0.1')).length, + }; + } finally { + window.fetch = originalFetch; + delete globalWindow.__wmFetchPatched; + if (previousTauri === undefined) { + delete globalWindow.__TAURI__; + } else { + globalWindow.__TAURI__ = previousTauri; + } + } + }); + + expect(result.fetchError).not.toBeNull(); + expect(result.cloudCalls).toBe(0); + expect(result.localCalls).toBeGreaterThan(0); + }); + + test('cloud fallback allowed with valid WorldMonitor API key', async ({ page }) => { + await page.goto('/tests/runtime-harness.html'); + + const result = await page.evaluate(async () => { + const runtime = await import('/src/services/runtime.ts'); + const runtimeConfig = await import('/src/services/runtime-config.ts'); + const globalWindow = window as unknown as Record; + const originalFetch = window.fetch.bind(window); + + const calls: string[] = []; + const capturedHeaders: Record = {}; + const responseJson = (body: unknown, status = 200) => + new Response(JSON.stringify(body), { + status, + headers: { 'content-type': 'application/json' }, + }); + + window.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const url = + typeof input === 'string' + ? input + : input instanceof URL + ? input.toString() + : input.url; + + calls.push(url); + + if (url.includes('worldmonitor.app') && init?.headers) { + const h = new Headers(init.headers); + const wmKey = h.get('X-WorldMonitor-Key'); + if (wmKey) capturedHeaders['X-WorldMonitor-Key'] = wmKey; + } + + if (url.includes('127.0.0.1:46123/api/market/v1/test')) { + throw new Error('ECONNREFUSED'); + } + if (url.includes('worldmonitor.app/api/market/v1/test')) { + return responseJson({ quotes: [] }, 200); + } + return responseJson({ ok: true }, 200); + }) as typeof window.fetch; + + const previousTauri = globalWindow.__TAURI__; + globalWindow.__TAURI__ = { core: { invoke: () => Promise.resolve(null) } }; + delete globalWindow.__wmFetchPatched; + + const testKey = 'wm_test_key_1234567890abcdef'; + await runtimeConfig.setSecretValue('WORLDMONITOR_API_KEY' as import('/src/services/runtime-config.ts').RuntimeSecretKey, testKey); + + try { + runtime.installRuntimeFetchPatch(); + + const response = await window.fetch('/api/market/v1/test'); + const body = await response.json() as { quotes?: unknown[] }; + + return { + status: response.status, + hasQuotes: Array.isArray(body.quotes), + cloudCalls: calls.filter(u => u.includes('worldmonitor.app')).length, + wmKeyHeader: capturedHeaders['X-WorldMonitor-Key'] || null, + }; + } finally { + window.fetch = originalFetch; + delete globalWindow.__wmFetchPatched; + if (previousTauri === undefined) { + delete globalWindow.__TAURI__; + } else { + globalWindow.__TAURI__ = previousTauri; + } + await runtimeConfig.setSecretValue('WORLDMONITOR_API_KEY' as import('/src/services/runtime-config.ts').RuntimeSecretKey, ''); + } + }); + + expect(result.status).toBe(200); + expect(result.hasQuotes).toBe(true); + expect(result.cloudCalls).toBe(1); + expect(result.wmKeyHeader).toBe('wm_test_key_1234567890abcdef'); + }); + test('country-instability HAPI fallback ignores eventsCivilianTargeting in score', async ({ page }) => { await page.goto('/tests/runtime-harness.html'); diff --git a/package-lock.json b/package-lock.json index 61bd55788..4f232c481 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "world-monitor", - "version": "2.5.1", + "version": "2.5.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "world-monitor", - "version": "2.5.1", + "version": "2.5.2", "license": "AGPL-3.0-only", "dependencies": { "@deck.gl/aggregation-layers": "^9.2.6", @@ -18,6 +18,7 @@ "@upstash/redis": "^1.36.1", "@vercel/analytics": "^1.6.1", "@xenova/transformers": "^2.17.2", + "convex": "^1.32.0", "d3": "^7.9.0", "deck.gl": "^9.2.6", "fast-xml-parser": "^5.3.7", @@ -35,6 +36,7 @@ "@types/maplibre-gl": "^1.13.2", "@types/topojson-client": "^3.1.5", "@types/topojson-specification": "^1.0.5", + "esbuild": "^0.27.3", "markdownlint-cli2": "^0.20.0", "typescript": "^5.7.2", "vite": "^6.0.7", @@ -2037,6 +2039,448 @@ "@luma.gl/core": "~9.2.6" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esri/arcgis-html-sanitizer": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/@esri/arcgis-html-sanitizer/-/arcgis-html-sanitizer-4.1.0.tgz", @@ -5314,6 +5758,518 @@ "dev": true, "license": "MIT" }, + "node_modules/convex": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/convex/-/convex-1.32.0.tgz", + "integrity": "sha512-5FlajdLpW75pdLS+/CgGH5H6yeRuA+ru50AKJEYbJpmyILUS+7fdTvsdTaQ7ZFXMv0gE8mX4S+S3AtJ94k0mfw==", + "license": "Apache-2.0", + "dependencies": { + "esbuild": "0.27.0", + "prettier": "^3.0.0", + "ws": "8.18.0" + }, + "bin": { + "convex": "bin/main.js" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=7.0.0" + }, + "peerDependencies": { + "@auth0/auth0-react": "^2.0.1", + "@clerk/clerk-react": "^4.12.8 || ^5.0.0", + "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@auth0/auth0-react": { + "optional": true + }, + "@clerk/clerk-react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/convex/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz", + "integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/android-arm": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz", + "integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/android-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz", + "integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/android-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz", + "integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz", + "integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/darwin-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz", + "integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz", + "integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz", + "integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/linux-arm": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz", + "integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/linux-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz", + "integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/linux-ia32": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz", + "integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/linux-loong64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz", + "integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz", + "integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz", + "integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz", + "integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/linux-s390x": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz", + "integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/linux-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz", + "integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz", + "integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz", + "integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz", + "integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz", + "integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz", + "integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/sunos-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz", + "integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/win32-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz", + "integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/win32-ia32": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz", + "integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/win32-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz", + "integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/esbuild": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz", + "integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.0", + "@esbuild/android-arm": "0.27.0", + "@esbuild/android-arm64": "0.27.0", + "@esbuild/android-x64": "0.27.0", + "@esbuild/darwin-arm64": "0.27.0", + "@esbuild/darwin-x64": "0.27.0", + "@esbuild/freebsd-arm64": "0.27.0", + "@esbuild/freebsd-x64": "0.27.0", + "@esbuild/linux-arm": "0.27.0", + "@esbuild/linux-arm64": "0.27.0", + "@esbuild/linux-ia32": "0.27.0", + "@esbuild/linux-loong64": "0.27.0", + "@esbuild/linux-mips64el": "0.27.0", + "@esbuild/linux-ppc64": "0.27.0", + "@esbuild/linux-riscv64": "0.27.0", + "@esbuild/linux-s390x": "0.27.0", + "@esbuild/linux-x64": "0.27.0", + "@esbuild/netbsd-arm64": "0.27.0", + "@esbuild/netbsd-x64": "0.27.0", + "@esbuild/openbsd-arm64": "0.27.0", + "@esbuild/openbsd-x64": "0.27.0", + "@esbuild/openharmony-arm64": "0.27.0", + "@esbuild/sunos-x64": "0.27.0", + "@esbuild/win32-arm64": "0.27.0", + "@esbuild/win32-ia32": "0.27.0", + "@esbuild/win32-x64": "0.27.0" + } + }, + "node_modules/convex/node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "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/core-assert": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/core-assert/-/core-assert-0.2.1.tgz", @@ -6424,6 +7380,48 @@ "benchmarks" ] }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -9566,6 +10564,21 @@ "node": ">=6" } }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-bytes": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", diff --git a/package.json b/package.json index a684376a7..c1be1943e 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,8 @@ "dev:tech": "VITE_VARIANT=tech vite", "dev:finance": "VITE_VARIANT=finance vite", "build": "tsc && vite build", + "build:sidecar-sebuf": "node scripts/build-sidecar-sebuf.mjs", + "build:desktop": "node scripts/build-sidecar-sebuf.mjs && tsc && vite build", "build:full": "VITE_VARIANT=full tsc && VITE_VARIANT=full vite build", "build:tech": "VITE_VARIANT=tech tsc && VITE_VARIANT=tech vite build", "build:finance": "VITE_VARIANT=finance tsc && VITE_VARIANT=finance vite build", @@ -52,6 +54,7 @@ "@types/maplibre-gl": "^1.13.2", "@types/topojson-client": "^3.1.5", "@types/topojson-specification": "^1.0.5", + "esbuild": "^0.27.3", "markdownlint-cli2": "^0.20.0", "typescript": "^5.7.2", "vite": "^6.0.7", @@ -68,6 +71,7 @@ "@upstash/redis": "^1.36.1", "@vercel/analytics": "^1.6.1", "@xenova/transformers": "^2.17.2", + "convex": "^1.32.0", "d3": "^7.9.0", "deck.gl": "^9.2.6", "fast-xml-parser": "^5.3.7", diff --git a/server/cors.ts b/server/cors.ts index 6491b4a85..8a3173ab2 100644 --- a/server/cors.ts +++ b/server/cors.ts @@ -36,7 +36,7 @@ export function getCorsHeaders(req: Request): Record { return { 'Access-Control-Allow-Origin': allowOrigin, 'Access-Control-Allow-Methods': 'POST, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-WorldMonitor-Key', 'Access-Control-Max-Age': '86400', 'Vary': 'Origin', }; diff --git a/settings.html b/settings.html index 604062bda..a2cbcb1b7 100644 --- a/settings.html +++ b/settings.html @@ -12,6 +12,7 @@ +

@@ -45,6 +46,9 @@
+
+
+