From e9fb50a2483a756101c1554be0386a0d8785f4c1 Mon Sep 17 00:00:00 2001 From: Rene Fichtmueller Date: Fri, 27 Mar 2026 16:27:31 +1300 Subject: [PATCH] =?UTF-8?q?feat:=20TIP=20Phase=200+1=20=E2=80=94=20monorep?= =?UTF-8?q?o,=20DB=20schema,=20API,=20scraper=20engine?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 0 - Foundation: - Restructure into npm workspace monorepo (packages/core, api, scraper) - PostgreSQL 17 + TimescaleDB schema (15 tables incl. hypertables) - Docker Compose for local dev (PostgreSQL on 5433 + Qdrant) - Express 5 API on port 3200 with 6 routes - Seed script to migrate 159 transceivers + 42 standards from npm package - Erik server setup script + PM2 ecosystem config Phase 1 - Scraper Engine: - Crawlee + Playwright framework with pg-boss scheduler - FS.com scraper (PlaywrightCrawler, anti-bot workaround) - Optcore.net scraper (WP REST API enumeration + PlaywrightCrawler) - Uses /wp-json/wp/v2/product to get 2000+ product URLs - Playwright renders individual product pages for price extraction - Cisco TMG Matrix scraper (compatibility data) - News RSS aggregator (optics.org, SPIE, Network World, Nature Photonics) - Keyword relevance scoring for transceiver/fiber topics - xml2js with malformed XML sanitization - SHA-256 content hashing for change detection (skip unchanged records) - pg-boss v10 with explicit queue creation before scheduling --- CONCEPT-transceiver-intelligence-platform.md | 1107 ++++ RESEARCH-optical-transceiver-history.md | 672 +++ docker-compose.yml | 33 + ecosystem.config.js | 16 + package-lock.json | 5660 +++++++++++++++++- package.json | 49 +- packages/api/package.json | 28 + packages/api/src/config.ts | 20 + packages/api/src/db/client.ts | 17 + packages/api/src/db/queries.ts | 211 + packages/api/src/index.ts | 57 + packages/api/src/routes/health.ts | 32 + packages/api/src/routes/standards.ts | 15 + packages/api/src/routes/switches.ts | 46 + packages/api/src/routes/transceivers.ts | 45 + packages/api/src/routes/vendors.ts | 15 + packages/api/tsconfig.json | 19 + packages/core/package.json | 45 + packages/core/src/breakouts.ts | 20 + packages/core/src/database.ts | 217 + packages/core/src/index.ts | 37 + packages/core/src/market.ts | 28 + packages/core/src/standards.ts | 82 + packages/core/src/types.ts | 137 + packages/core/tsconfig.json | 20 + packages/scraper/package.json | 31 + packages/scraper/src/index.ts | 69 + packages/scraper/src/scheduler.ts | 127 + packages/scraper/src/scrapers/cisco-tmg.ts | 155 + packages/scraper/src/scrapers/fs-com.ts | 277 + packages/scraper/src/scrapers/news.ts | 269 + packages/scraper/src/scrapers/optcore.ts | 297 + packages/scraper/src/utils/db.ts | 123 + packages/scraper/src/utils/hash.ts | 71 + packages/scraper/tsconfig.json | 18 + scripts/migrate.ts | 42 + scripts/seed-from-npm.ts | 280 + scripts/setup-erik.sh | 73 + sql/001-extensions.sql | 7 + sql/002-core-tables.sql | 469 ++ sql/003-timeseries.sql | 122 + sql/004-indexes.sql | 75 + 42 files changed, 11096 insertions(+), 37 deletions(-) create mode 100644 CONCEPT-transceiver-intelligence-platform.md create mode 100644 RESEARCH-optical-transceiver-history.md create mode 100644 docker-compose.yml create mode 100644 ecosystem.config.js create mode 100644 packages/api/package.json create mode 100644 packages/api/src/config.ts create mode 100644 packages/api/src/db/client.ts create mode 100644 packages/api/src/db/queries.ts create mode 100644 packages/api/src/index.ts create mode 100644 packages/api/src/routes/health.ts create mode 100644 packages/api/src/routes/standards.ts create mode 100644 packages/api/src/routes/switches.ts create mode 100644 packages/api/src/routes/transceivers.ts create mode 100644 packages/api/src/routes/vendors.ts create mode 100644 packages/api/tsconfig.json create mode 100644 packages/core/package.json create mode 100644 packages/core/src/breakouts.ts create mode 100644 packages/core/src/database.ts create mode 100644 packages/core/src/index.ts create mode 100644 packages/core/src/market.ts create mode 100644 packages/core/src/standards.ts create mode 100644 packages/core/src/types.ts create mode 100644 packages/core/tsconfig.json create mode 100644 packages/scraper/package.json create mode 100644 packages/scraper/src/index.ts create mode 100644 packages/scraper/src/scheduler.ts create mode 100644 packages/scraper/src/scrapers/cisco-tmg.ts create mode 100644 packages/scraper/src/scrapers/fs-com.ts create mode 100644 packages/scraper/src/scrapers/news.ts create mode 100644 packages/scraper/src/scrapers/optcore.ts create mode 100644 packages/scraper/src/utils/db.ts create mode 100644 packages/scraper/src/utils/hash.ts create mode 100644 packages/scraper/tsconfig.json create mode 100644 scripts/migrate.ts create mode 100644 scripts/seed-from-npm.ts create mode 100755 scripts/setup-erik.sh create mode 100644 sql/001-extensions.sql create mode 100644 sql/002-core-tables.sql create mode 100644 sql/003-timeseries.sql create mode 100644 sql/004-indexes.sql diff --git a/CONCEPT-transceiver-intelligence-platform.md b/CONCEPT-transceiver-intelligence-platform.md new file mode 100644 index 0000000..620711e --- /dev/null +++ b/CONCEPT-transceiver-intelligence-platform.md @@ -0,0 +1,1107 @@ +# Transceiver Intelligence Platform (TIP) +## Das massivste Transceiver & Switch Database Projekt der Welt + +**Version:** 1.0 — 2026-03-27 +**Author:** Rene Fichtmueller / Context X +**Repository:** github.com/renefichtmueller/transceiver-db +**Integration:** EO Global Pulse (Flexoptix) + +--- + +## 1. VISION + +Eine lebendige Intelligence-Plattform die alles vereint: +- **159+ Transceiver** (bestehende npm DB) als Seed-Daten +- **Echtzeit-Preisüberwachung** aller Wettbewerber weltweit +- **Lagerbestände live** von FS.com bis chinesischen OEMs +- **Digitalisierte Handbücher** aller 400+ Switch-Hersteller per MCP durchsuchbar +- **Hype Cycle Engine** basierend auf Norton-Bass Multigenerational Diffusion Model +- **FAQ/Troubleshooting Knowledge Base** aus gescannten Vendor-FAQs +- **Automatische Blog-Generierung** aus Marktdaten und Trends +- **Freitext-Suche** für Sales ("Kunde hat Cisco Nexus 93180, braucht 10km Transceiver") +- **Template-Matching** (FlexBox Coding + Switch-Konfiguration) +- **News-Aggregation** von OFC, ECOC, CIOE, Photonics West +- **Fabrik-Monitoring** aller Transceiver-Hersteller weltweit + +**Kein statisches Nachschlagewerk — eine sich selbst aktualisierende Competitive Intelligence Engine.** + +--- + +## 2. ARCHITEKTUR + +### 2.1 System-Übersicht + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ CLOUDFLARE EDGE LAYER │ +│ │ +│ Workers R2 Storage KV Cache Queues │ +│ ┌─────────┐ ┌──────────┐ ┌─────────┐ ┌──────────┐ │ +│ │ API GW │ │ PDFs │ │ Hot │ │ Scrape │ │ +│ │ MCP Srv │ │ Manuals │ │ Product │ │ Jobs │ │ +│ │ FAQ Chat│ │ Images │ │ Cache │ │ OCR Pipe │ │ +│ │ Blog API│ │ Exports │ │ Prices │ │ Alerts │ │ +│ └─────────┘ └──────────┘ └─────────┘ └──────────┘ │ +│ │ │ │ │ │ +└─────────┼──────────────┼──────────────────┼───────────────┼─────────────┘ + │ Cloudflare Tunnel │ │ +┌─────────▼──────────────▼──────────────────▼───────────────▼─────────────┐ +│ ERIK SERVER (.82) — Core Engine │ +│ │ +│ ┌───────────────────────────────────────────────────────────────────┐ │ +│ │ PostgreSQL 17 + TimescaleDB + pgvector │ │ +│ │ │ │ +│ │ CORE TABLES: │ │ +│ │ vendors · transceivers · switches · compatibility · │ │ +│ │ documents · knowledge_base · standards · form_factors · │ │ +│ │ templates · factories · news_articles · blog_drafts │ │ +│ │ │ │ +│ │ TIME-SERIES (Hypertables): │ │ +│ │ price_observations · stock_observations · market_metrics │ │ +│ │ │ │ +│ │ FULL-TEXT SEARCH: │ │ +│ │ tsvector auf allen durchsuchbaren Feldern │ │ +│ └───────────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌───────────────────────────────────────────────────────────────────┐ │ +│ │ Qdrant (Docker, Port 6333) — Vector Search Engine │ │ +│ │ │ │ +│ │ Collections: │ │ +│ │ product_embeddings · datasheet_chunks · faq_embeddings · │ │ +│ │ manual_chunks · troubleshooting_embeddings · news_embeddings │ │ +│ │ │ │ +│ │ Payload Filtering: vendor, form_factor, speed, category, reach │ │ +│ └───────────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌───────────────────────────────────────────────────────────────────┐ │ +│ │ Scraper Orchestrator (Node.js + Crawlee + Playwright, PM2) │ │ +│ │ │ │ +│ │ Job Scheduler: pg-boss (PostgreSQL-backed) │ │ +│ │ Echtzeit: Preise + Stock (5-15 Min Intervall) │ │ +│ │ Täglich: News, Messen, Blog-Feeds │ │ +│ │ Wöchentlich: Handbücher, Datasheets, FAQs │ │ +│ │ Monatlich: Factory-Updates, Vendor-Websites │ │ +│ │ Change Detection: SHA-256 Content Hashing │ │ +│ │ Adaptive Scheduling: Häufige Änderungen → kürzere Intervalle │ │ +│ └───────────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌───────────────────────────────────────────────────────────────────┐ │ +│ │ Hype Cycle Engine (TypeScript) │ │ +│ │ │ │ +│ │ Norton-Bass Multigenerational Diffusion Model │ │ +│ │ + Media Sentiment Analysis │ │ +│ │ + Phase Classification (5 Phasen) │ │ +│ │ = Automatische Hype Cycle Berechnung pro Technologie │ │ +│ └───────────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌───────────────────────────────────────────────────────────────────┐ │ +│ │ OCR Pipeline: Docling → Chunk → Embed → Store │ │ +│ │ Embeddings: Ollama nomic-embed-text auf .213 (zero cost) │ │ +│ └───────────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────────┐ +│ MAC STUDIO (.213) — AI Layer │ +│ Ollama: nomic-embed-text (Embeddings), qwen2.5:14b (Blog/FAQ) │ +└─────────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────────┐ +│ EO GLOBAL PULSE — Frontend │ +│ Transceiver LLM Chat · Sales Advisor · Hype Cycle Dashboard │ +│ Compatibility Checker · Price Comparison · Template Finder │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### 2.2 Warum diese Architektur + +| Anforderung | Komponente | Begründung | +|---|---|---| +| Relationale Daten | PostgreSQL 17 | Bewährt, Team kennt es, FK-Constraints, ACID | +| Zeitreihen (Preise/Stock) | TimescaleDB Extension | Hypertable-Kompression (20x), Continuous Aggregates für Trends | +| Vektor-Suche mit Filtern | Qdrant | Payload-Filtering: "Zeige Transceiver WHERE vendor=Cisco AND form_factor=QSFP28 ORDER BY similarity" — pgvector kann das nicht effizient | +| Dokument-Storage | Cloudflare R2 | S3-kompatibel, keine Egress-Kosten, global verfügbar | +| Hot-Cache | Cloudflare KV | Sub-ms Reads für häufig abgefragte Produkte | +| API Layer | Cloudflare Workers | Global Edge, MCP-Server-Host, Multi-Tenant Routing | +| Scraping | Crawlee + Playwright auf Erik | Crawlee (18k+ Stars) — Request-Queuing, Auto-Retry, Anti-Bot, Session-Management out of the box | +| OCR | Docling v2.81 (bereits installiert) | Beste OSS-Option für technische PDFs mit Tabellen | +| Embeddings | Ollama nomic-embed-text auf .213 | 768 Dimensionen, 8192 Token Context, zero API-Kosten | +| Blog-Generierung | Ollama qwen2.5:14b auf .213 | Lokale LLM-Generierung, keine API-Kosten | + +### 2.3 Kosten (monatlich) + +| Komponente | Hosting | Kosten | +|---|---|---| +| PostgreSQL + TimescaleDB + Qdrant | Erik (.82, existiert) | 0 EUR | +| Cloudflare Workers (API) | Workers Paid Plan | ~5 EUR | +| Cloudflare R2 (~100GB PDFs/Manuals) | R2 Storage | ~1.50 EUR | +| Cloudflare KV (Hot Cache) | KV Namespace | ~0.50 EUR | +| Residential Proxies (Scraping) | Bright Data / Geonode | 50-100 EUR | +| Ollama Embeddings + LLM | Mac Studio .213 (existiert) | 0 EUR (Strom) | +| **Gesamt** | | **~55-105 EUR/Monat** | + +--- + +## 3. DATENBANK-SCHEMA + +### 3.1 Core Tables (PostgreSQL) + +```sql +-- ============================================================ +-- VENDORS (Hersteller, Distributoren, Reseller, OEMs) +-- ============================================================ +CREATE TABLE vendors ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL UNIQUE, + slug TEXT NOT NULL UNIQUE, -- URL-safe identifier + type TEXT NOT NULL, -- manufacturer|distributor|oem|reseller|compatible + headquarters TEXT, + country TEXT, + website TEXT, + shop_url TEXT, -- Webshop URL für Scraping + api_available BOOLEAN DEFAULT FALSE, + api_endpoint TEXT, + logo_r2_key TEXT, + founded_year INTEGER, + revenue_usd BIGINT, -- Letzte bekannte Jahresumsatz + employee_count INTEGER, + market_position TEXT, -- "Top 3 Global", "Regional Leader", etc. + specialties TEXT[], -- ["400G", "Coherent", "CWDM"] + scrape_config JSONB, -- Scraper-spezifische Config (Selektoren, etc.) + last_scraped TIMESTAMPTZ, + is_competitor BOOLEAN DEFAULT FALSE, -- Wettbewerber von Flexoptix + is_factory BOOLEAN DEFAULT FALSE, -- Eigene Fertigung + factory_locations TEXT[], -- ["Shenzhen", "Thailand", "Vietnam"] + certifications TEXT[], -- ["ISO 9001", "RoHS", "TAA"] + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- ============================================================ +-- TRANSCEIVERS (Haupttabelle — erweitert bestehende npm DB) +-- ============================================================ +CREATE TABLE transceivers ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + vendor_id UUID REFERENCES vendors(id), + part_number TEXT NOT NULL, + standard TEXT, -- IEEE/MSA Referenz + form_factor TEXT NOT NULL, -- SFP, SFP+, QSFP28, QSFP-DD, OSFP... + speed_gbps NUMERIC NOT NULL, + speed_label TEXT, -- "100G", "400G", "800G" + lanes INTEGER, + lane_rate_gbps NUMERIC, + modulation TEXT, -- NRZ, PAM4, Coherent + reach_meters INTEGER NOT NULL, + reach_label TEXT, -- "SR", "LR", "ER", "ZR" + fiber_type TEXT, -- SMF, MMF, Copper + wavelengths TEXT, -- "850nm", "1310nm", "CWDM4" + connector TEXT, -- LC, SC, MPO-12, MPO-16, CS, SN + power_consumption_w NUMERIC, + temp_range TEXT, -- COM (-0/70°C), IND (-40/85°C) + category TEXT, -- DataCenter, Metro, LongHaul, DCI, CWDM, DWDM... + dom_support BOOLEAN DEFAULT TRUE, + digital_diagnostics TEXT, -- DDM, CMIS, SFF-8472, SFF-8636 + + -- CWDM/DWDM spezifisch + wdm_type TEXT, -- CWDM, DWDM, null für non-WDM + channel_count INTEGER, -- Anzahl Wellenlängen + channel_spacing_ghz NUMERIC, -- 100GHz, 50GHz für DWDM + tunable BOOLEAN DEFAULT FALSE, + itu_grid TEXT, -- ITU-T G.694.1/G.694.2 + + -- Coherent spezifisch + coherent BOOLEAN DEFAULT FALSE, + baud_rate_gbaud NUMERIC, + fec_type TEXT, -- oFEC, cFEC, KP4, RS-FEC + dsp_vendor TEXT, -- Broadcom, Marvell, Inphi + + -- Lifecycle & Markt + year_introduced INTEGER, + year_mainstream INTEGER, -- Wann Massenmarkt erreicht + year_peak INTEGER, -- Peak-Shipments + year_decline INTEGER, -- Beginn Rückgang + market_status TEXT, -- Mainstream, Growth, Emerging, Legacy, EOL + hype_cycle_phase TEXT, -- InnovationTrigger, Peak, Trough, Slope, Plateau + generation TEXT, -- "Gen1", "Gen2", "Next-Gen" + + -- Pricing + price_tier TEXT, -- Budget, Standard, Premium + msrp_usd NUMERIC, -- Listenpreis + street_price_usd NUMERIC, -- Aktueller Marktpreis (von Scraper) + + -- Technische Details + operating_case_temp TEXT, + max_link_length TEXT, + optical_budget_db NUMERIC, + tx_power_min_dbm NUMERIC, + tx_power_max_dbm NUMERIC, + rx_sensitivity_dbm NUMERIC, + extinction_ratio_db NUMERIC, + + -- Meta + datasheet_r2_key TEXT, -- PDF in R2 + image_r2_key TEXT, + tags TEXT[], + notes TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + + UNIQUE(vendor_id, part_number) +); + +-- ============================================================ +-- SWITCHES (Alle Netzwerk-Switches aller Hersteller) +-- ============================================================ +CREATE TABLE switches ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + vendor_id UUID REFERENCES vendors(id), + model TEXT NOT NULL, + series TEXT, -- "Nexus 9000", "QFX5000", "7280R3" + category TEXT, -- DataCenter, Campus, Edge, Core, SP + layer TEXT, -- L2, L3, L2/L3 + managed BOOLEAN DEFAULT TRUE, + + -- Port-Konfiguration + ports_config JSONB, -- {"48x10G SFP+": 48, "6x100G QSFP28": 6} + total_ports INTEGER, + uplink_speed_gbps NUMERIC, + max_speed_gbps NUMERIC, -- Höchste Port-Geschwindigkeit + + -- Leistungsdaten + switching_capacity_tbps NUMERIC, + forwarding_rate_mpps NUMERIC, + latency_ns NUMERIC, + buffer_mb NUMERIC, + + -- ASIC Info + asic_vendor TEXT, -- Broadcom, Marvell, Cisco, NVIDIA + asic_model TEXT, -- Tomahawk 4, Jericho2, Silicon One + asic_generation TEXT, + + -- Features + poe_support TEXT, -- None, PoE, PoE+, PoE++, UPoE + stacking_support BOOLEAN DEFAULT FALSE, + vxlan_support BOOLEAN DEFAULT FALSE, + evpn_support BOOLEAN DEFAULT FALSE, + bgp_support BOOLEAN DEFAULT FALSE, + mpls_support BOOLEAN DEFAULT FALSE, + openconfig_support BOOLEAN DEFAULT FALSE, + sonic_compatible BOOLEAN DEFAULT FALSE, + macsec_support BOOLEAN DEFAULT FALSE, + + -- Lifecycle + release_date DATE, + eos_date DATE, -- End of Sale + eol_date DATE, -- End of Life + last_support_date DATE, + lifecycle_status TEXT, -- Active, EoS Announced, EoL, Legacy + successor_model TEXT, + + -- Physisch + rack_units NUMERIC, + max_power_w NUMERIC, + typical_power_w NUMERIC, + weight_kg NUMERIC, + airflow TEXT, -- F2B, B2F + + -- Pricing + msrp_usd NUMERIC, + street_price_usd NUMERIC, + + -- Dokumentation + manual_r2_key TEXT, -- Digitalisiertes Handbuch in R2 + datasheet_r2_key TEXT, + config_guide_r2_key TEXT, + compatibility_list_url TEXT, -- Vendor Compatibility Matrix URL + + -- Meta + tags TEXT[], + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + + UNIQUE(vendor_id, model) +); + +-- ============================================================ +-- COMPATIBILITY (Switch ↔ Transceiver Kompatibilität) +-- ============================================================ +CREATE TABLE compatibility ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + switch_id UUID REFERENCES switches(id), + transceiver_id UUID REFERENCES transceivers(id), + verified_by TEXT, -- "Flexoptix Lab", "Vendor Matrix", "Community" + verification_date DATE, + verification_method TEXT, -- "tested", "vendor_matrix", "datasheet", "community" + status TEXT DEFAULT 'compatible', -- compatible, incompatible, partial, unknown + notes TEXT, + firmware_min TEXT, -- Minimum Firmware für Kompatibilität + known_issues TEXT, + source_url TEXT, -- Link zur Quelle + created_at TIMESTAMPTZ DEFAULT NOW(), + + UNIQUE(switch_id, transceiver_id) +); + +-- ============================================================ +-- TEMPLATES (FlexBox Coding + Switch-Konfiguration) +-- ============================================================ +CREATE TABLE templates ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + type TEXT NOT NULL, -- flexbox_coding | switch_config + name TEXT NOT NULL, + description TEXT, + switch_vendor TEXT, -- Für welchen Switch-Hersteller + switch_series TEXT, -- Für welche Switch-Serie + transceiver_type TEXT, -- Für welchen Transceiver-Typ + speed_gbps NUMERIC, + technology TEXT, -- CWDM, DWDM, SR, LR, ZR + template_content TEXT NOT NULL, -- Der eigentliche Template-Code/Config + variables JSONB, -- Platzhalter-Variablen + tags TEXT[], + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- ============================================================ +-- DOCUMENTS (PDFs, Handbücher, Datasheets in R2) +-- ============================================================ +CREATE TABLE documents ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + entity_type TEXT NOT NULL, -- transceiver | switch | vendor | standard + entity_id UUID, + doc_type TEXT NOT NULL, -- manual | datasheet | config_guide | compatibility_list | faq | whitepaper + title TEXT, + filename TEXT, + r2_key TEXT NOT NULL, -- Pfad in R2 + source_url TEXT, -- Original-Download-URL + file_size_bytes BIGINT, + page_count INTEGER, + ocr_status TEXT DEFAULT 'pending', -- pending | processing | completed | failed + ocr_text TEXT, -- Extrahierter Volltext + language TEXT DEFAULT 'en', + content_hash TEXT, -- SHA-256 für Change Detection + last_checked TIMESTAMPTZ, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- ============================================================ +-- KNOWLEDGE BASE (FAQs, Troubleshooting, Best Practices) +-- ============================================================ +CREATE TABLE knowledge_base ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + category TEXT NOT NULL, -- troubleshooting | faq | best_practice | known_issue | compatibility_tip + subcategory TEXT, -- optical_power | link_flap | temperature | firmware | interop + question TEXT NOT NULL, + answer TEXT NOT NULL, + source_vendor TEXT, -- Von welchem Vendor-FAQ gescannt + source_url TEXT, + applies_to_form_factors TEXT[], -- ["QSFP28", "QSFP-DD"] + applies_to_speeds TEXT[], -- ["100G", "400G"] + applies_to_vendors TEXT[], -- ["Cisco", "Juniper"] + severity TEXT, -- critical | high | medium | low | info + resolution_steps JSONB, -- Strukturierte Lösungsschritte + last_verified TIMESTAMPTZ, + helpful_count INTEGER DEFAULT 0, -- Upvotes von Nutzern + tags TEXT[], + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- ============================================================ +-- STANDARDS (IEEE, OIF, MSA — erweitert bestehende npm DB) +-- ============================================================ +CREATE TABLE standards ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL UNIQUE, -- "IEEE 802.3bs", "OIF 400ZR" + body TEXT, -- IEEE, OIF, MSA + type TEXT, -- ieee_amendment | oif_ia | msa | de_facto + speed_gbps NUMERIC, + lanes INTEGER, + lane_rate_gbps NUMERIC, + modulation TEXT, + fiber_type TEXT, + wavelength TEXT, + max_reach_meters INTEGER, + fec_required BOOLEAN, + form_factors TEXT[], + year_draft INTEGER, + year_ratified INTEGER, + year_revised INTEGER, + status TEXT, -- draft | ratified | revised | superseded + superseded_by TEXT, + member_count INTEGER, -- Für MSAs + notes TEXT, + url TEXT, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- ============================================================ +-- FACTORIES (Fertigungsstätten weltweit) +-- ============================================================ +CREATE TABLE factories ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + vendor_id UUID REFERENCES vendors(id), + name TEXT NOT NULL, + country TEXT NOT NULL, + city TEXT, + factory_type TEXT, -- manufacturing | assembly | r_and_d | headquarters + products TEXT[], -- ["SFP+", "QSFP28", "Coherent"] + capacity_units_month INTEGER, -- Geschätzte Monatskapazität + employee_count INTEGER, + certifications TEXT[], + expansion_planned BOOLEAN DEFAULT FALSE, + expansion_details TEXT, + source_url TEXT, + last_verified TIMESTAMPTZ, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- ============================================================ +-- NEWS (Aggregierte Nachrichten von Messen, Trade Press) +-- ============================================================ +CREATE TABLE news_articles ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + title TEXT NOT NULL, + source TEXT NOT NULL, -- "Lightwave", "Light Reading", "OFC 2026" + source_url TEXT NOT NULL UNIQUE, + published_at TIMESTAMPTZ, + author TEXT, + summary TEXT, + full_text TEXT, + category TEXT, -- product_launch | market_report | standard | m_and_a | factory | event + event TEXT, -- OFC, ECOC, CIOE, Photonics West + mentioned_vendors TEXT[], + mentioned_products TEXT[], + mentioned_standards TEXT[], + sentiment_score NUMERIC, -- -1.0 bis +1.0 + relevance_score NUMERIC, -- 0-100 + content_hash TEXT, + tags TEXT[], + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- ============================================================ +-- BLOG DRAFTS (Auto-generierte Entwürfe) +-- ============================================================ +CREATE TABLE blog_drafts ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + title TEXT NOT NULL, + topic TEXT, -- hype_cycle | price_trend | new_product | comparison | tutorial + target_audience TEXT, -- sales | technical | customer | seo + outline JSONB, + draft_content TEXT, + data_sources JSONB, -- Welche DB-Queries haben den Draft gefüttert + status TEXT DEFAULT 'draft', -- draft | review | approved | published + generated_by TEXT, -- "hype_cycle_engine" | "price_alert" | "news_digest" + word_count INTEGER, + seo_keywords TEXT[], + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); +``` + +### 3.2 TimescaleDB Hypertables (Echtzeit-Zeitreihen) + +```sql +-- ============================================================ +-- PRICE OBSERVATIONS (Echtzeit-Preise aller Wettbewerber) +-- ============================================================ +CREATE TABLE price_observations ( + time TIMESTAMPTZ NOT NULL, + transceiver_id UUID NOT NULL, + source_vendor_id UUID NOT NULL, -- Von welchem Shop/Vendor + price NUMERIC NOT NULL, + currency TEXT DEFAULT 'USD', + stock_level TEXT, -- "in_stock", "low_stock", "out_of_stock", "on_request" + quantity_available INTEGER, + lead_time_days INTEGER, + min_order_qty INTEGER, + url TEXT, -- Direkt-Link zum Produkt + content_hash TEXT -- Change Detection +); + +SELECT create_hypertable('price_observations', 'time', + chunk_time_interval => INTERVAL '1 day'); + +-- Continuous Aggregate für tägliche Durchschnitte +CREATE MATERIALIZED VIEW price_daily +WITH (timescaledb.continuous) AS +SELECT + time_bucket('1 day', time) AS bucket, + transceiver_id, + source_vendor_id, + AVG(price) AS avg_price, + MIN(price) AS min_price, + MAX(price) AS max_price, + last(stock_level, time) AS latest_stock, + COUNT(*) AS observation_count +FROM price_observations +GROUP BY bucket, transceiver_id, source_vendor_id; + +-- Retention Policy: Rohdaten 90 Tage, Aggregates 5 Jahre +SELECT add_retention_policy('price_observations', INTERVAL '90 days'); + +-- ============================================================ +-- MARKET METRICS (Hype Cycle Input-Daten) +-- ============================================================ +CREATE TABLE market_metrics ( + time TIMESTAMPTZ NOT NULL, + technology TEXT NOT NULL, -- "800G OSFP", "400G QSFP-DD", "CPO" + metric_type TEXT NOT NULL, -- vendor_count | shipment_share | asp_decline_rate | media_hype_index | patent_filings + value NUMERIC NOT NULL, + source TEXT, -- "LightCounting", "Crehan Research", "Google Trends" + notes TEXT +); + +SELECT create_hypertable('market_metrics', 'time', + chunk_time_interval => INTERVAL '30 days'); +``` + +### 3.3 Qdrant Vector Collections + +``` +Collection: product_embeddings + - vector: 768 dim (nomic-embed-text) + - payload: {product_id, vendor, form_factor, speed_gbps, category, fiber_type, reach_label} + - Use: Freitext-Suche ("10km Transceiver für Cisco Nexus") + +Collection: datasheet_chunks + - vector: 768 dim + - payload: {document_id, entity_type, entity_id, page, section, chunk_text_preview} + - Use: "Was ist der Tx-Power Range des QSFP-DD 400G LR4?" + +Collection: manual_chunks + - vector: 768 dim + - payload: {document_id, switch_vendor, switch_model, section, topic} + - Use: "Wie konfiguriere ich DWDM auf Juniper MX?" + +Collection: faq_embeddings + - vector: 768 dim + - payload: {kb_id, category, subcategory, question_preview, severity} + - Use: "Transceiver zeigt niedrige Rx Power, was tun?" + +Collection: news_embeddings + - vector: 768 dim + - payload: {article_id, source, published_at, category, event} + - Use: "Was wurde auf der OFC 2026 über 1.6T vorgestellt?" +``` + +--- + +## 4. DATENQUELLEN & CRAWLER-STRATEGIE + +### 4.1 Echtzeit-Preise & Stock (5-15 Min Intervalle) + +| Quelle | Typ | Methode | Preise | Stock | Besonderheiten | +|--------|-----|---------|--------|-------|----------------| +| **FS.com** | Reseller | Web Scraping (Crawlee/Playwright) | Öffentlich | Öffentlich | Größter Wettbewerber, transparenteste Preise | +| **Optcore** | Reseller | Web Scraping | Öffentlich (ab $5.50) | Öffentlich | Sehr transparente Preisgestaltung | +| **QSFPTEK** | Reseller | Web Scraping | Öffentlich | Öffentlich | Chinesischer Reseller | +| **10Gtek** | Reseller/Mfr | Web Scraping | Öffentlich | Öffentlich | Eigene Fertigung in China | +| **Edgeium** | Reseller | Web Scraping (Optics Finder) | Öffentlich | Öffentlich | Optics Finder Tool durchsuchbar | +| **Hummingbird Networks** | Reseller | Web Scraping | Öffentlich | Öffentlich | US-Reseller | +| **EDGE Optical** | Reseller | Web Scraping | Öffentlich | Öffentlich | | +| **ProLabs** | Compatible | Web Scraping | Quote-based | Begrenzt | Legrand-Tochter | +| **Champion ONE** | Compatible | Web Scraping | Quote-based | Begrenzt | Legrand-Tochter | +| **Smartoptics** | Reseller | Web Scraping | Quote-based | Auf Anfrage | Fokus DWDM | +| **NADDOD** | Reseller | Web Scraping | Öffentlich | Öffentlich | Chinesischer Reseller | +| **Solid Optics** | Compatible | Web Scraping | Öffentlich | Begrenzt | | +| **AddOn Networks** | Compatible | Web Scraping | Öffentlich | Begrenzt | | +| **Worldwide Supply** | Refurbished | Web Scraping | Quote-based | Auf Anfrage | Refurbished OEM-Optics | +| **Nexar/Octopart API** | Aggregator | GraphQL API | Ja (317 Distributoren) | Ja | Einzige echte API, aber optische Module evtl. begrenzt | + +### 4.2 OEM-Kompatibilitätstools (Wöchentlich) + +| Quelle | URL / Tool | Methode | +|--------|-----------|---------| +| **Cisco TMG Matrix** | tmgmatrix.cisco.com | Web Scraping — durchsuchbar nach Device/Transceiver/Speed/Reach | +| **Juniper HCT** | apps.juniper.net/hct | Web Scraping — durchsuchbar nach Produkt/Kategorie | +| **Arista** | arista.com (PDF Guides) | PDF Download → Docling OCR | +| **HPE/Aruba** | support.hpe.com | Web Scraping — QuickSpecs Portal | +| **Dell** | dell.com/support | Web Scraping | +| **Nokia** | Behind Customer Portal | Begrenzt — öffentliche Specs scrapen | +| **Extreme Networks** | extremenetworks.com | Web Scraping | +| **Huawei** | support.huawei.com | Web Scraping — Compatibility Matrix | + +### 4.3 Transceiver-Hersteller weltweit (Monatlich) + +**Top-Tier (Revenue > $500M):** +| Hersteller | Land | Methode | Fokus | +|-----------|------|---------|-------| +| InnoLight | China | Website + Press Releases | #1 weltweit, $3.3B Revenue, 114% Wachstum | +| Coherent (II-VI/Finisar) | USA | Investor Relations + Website | Kohärente Optik, VCSEL | +| Lumentum | USA/Thailand | Investor Relations + Website | Laser, 3D-Sensing, AI DC | +| Eoptolink | China | Website | #3 weltweit, $1.2B, 175% Wachstum | +| Broadcom (ex-Avago) | USA | Website | Embedded Optics, ASIC | +| Cisco (Acacia) | USA | Website + TMG Matrix | Kohärente Optik nach Acacia-Übernahme | +| Zhongji Innolight | China | Website | Cloud-fokussiert | + +**Mid-Tier ($50M - $500M):** +| Hersteller | Land | Methode | +|-----------|------|---------| +| Source Photonics | China | Website | +| Accelink | China | Website | +| HGGenuine | China | Website — einziger vertikal integrierter chinesischer Hersteller | +| Gigalight | China | Website | +| ATOP Technology | China/Malaysia | Website | +| Wolontek | China | Website | +| ETU-Link | China | Website | +| Skylane Optics | Belgien | Website | +| Smartoptics | Norwegen | Website | +| EFFECT Photonics | Niederlande | Website — Silicon Photonics | +| Sicoya/Marvell | Deutschland/USA | Website — Silicon Photonics | + +**SE-Asien Fabriken (Vierteljährlich):** +| Land | Fabriken | Output | +|------|----------|--------| +| Thailand | Lumentum (6000+ MA), Fabrinet, InnoLight/Terahop, Eoptolink | >800k units/Monat | +| Vietnam | Coherent (Primary non-China Hub) | >400k units/Monat | +| Malaysia | Coherent (R&D), ATOP (Manufacturing) | >300k units/Monat | + +### 4.4 News & Messen (Täglich) + +| Quelle | Typ | Methode | Update | +|--------|-----|---------|--------| +| **Lightwave** | Trade Press | RSS + Scraping | Täglich | +| **Light Reading** | Trade Press | RSS + Scraping | Täglich | +| **Fibre Systems** | Trade Press | RSS + Scraping | Täglich | +| **Laser Focus World** | Trade Press | RSS + Scraping | Täglich | +| **optics.org** | Trade Press | RSS + Scraping | Täglich | +| **Data Center Dynamics** | Trade Press | RSS + Scraping | Täglich | +| **SDxCentral** | Trade Press | RSS + Scraping | Täglich | +| **OFC** | Leitmesse | Post-Event Scraping | Jährlich (März) | +| **ECOC** | Leitmesse | Post-Event Scraping | Jährlich (September) | +| **CIOE** | Leitmesse (China) | Post-Event Scraping | Jährlich (September) | +| **Photonics West** | Leitmesse | Post-Event Scraping | Jährlich (Januar) | +| **Google Trends** | Hype Tracking | API | Wöchentlich | + +### 4.5 FAQ/Troubleshooting-Quellen (Wöchentlich) + +| Quelle | URL | Content | +|--------|-----|---------| +| FS.com Knowledge Center | community.fs.com | Troubleshooting Guides, Best Practices | +| FS.com Transceiver Manual | resource.fs.com (PDF) | 1G/10G/25G/40G/100G Troubleshooting, DDM | +| FluxLight Blog | blog.fluxlight.com | Transceiver Troubleshooting | +| Cisco Support Forums | community.cisco.com | Transceiver-Probleme, Kompatibilität | +| Juniper Knowledge Base | kb.juniper.net | Transceiver Issues | +| Arista Docs | arista.com/docs | Optics Support | +| Wolontek Guides | wolontek.com | OTDR Testing, Power Meter Guides | +| Reddit r/networking | reddit.com/r/networking | Community-Wissen, Erfahrungsberichte | +| NetworkEngineering SE | networkengineering.stackexchange.com | Q&A | + +### 4.6 Change Detection Strategie + +``` +Für JEDEN Scrape-Zyklus: +1. HTTP Request mit If-None-Match / If-Modified-Since Headers +2. Wenn 304 Not Modified → Skip (kein Bandwidth-Kosten) +3. Wenn 200 → Extrahiere Preis/Stock → SHA-256 Hash berechnen +4. Hash vergleichen mit gespeichertem Hash +5. Wenn Hash geändert → Write to TimescaleDB + Trigger Alert +6. Wenn Hash unverändert → Nur last_checked Timestamp updaten + +Adaptive Scheduling: +- Produkte mit häufigen Änderungen → 5 Min Intervall +- Produkte stabil seit 7+ Tagen → 30 Min Intervall +- Produkte stabil seit 30+ Tagen → 2 Std Intervall +→ Reduziert DB-Writes um 80-90% +``` + +--- + +## 5. HYPE CYCLE ENGINE + +### 5.1 Mathematisches Modell: Norton-Bass Multigenerational Diffusion + +Das Norton-Bass Model (1987, Management Science) ist die quantitative Basis, weil es sukzessive Generationen (1G → 10G → 40G → 100G → 400G → 800G → 1.6T) nativ modelliert. + +**Kern-Gleichung:** +``` +S(t) = L / (1 + e^(-k(t - t0))) + +Wobei: +- L = Marktpotenzial (Total Addressable Port Shipments) +- k = Wachstumsrate +- t0 = Inflection Point +- S(t) = Kumulative Adoption zum Zeitpunkt t +``` + +**Bass Diffusion Parameter:** +``` +f(t) / [1 - F(t)] = p + q * F(t) + +p = Innovationskoeffizient (~0.03 für Netzwerk-Hardware) +q = Imitationskoeffizient (~0.3-0.5) +m = Gesamtmarktpotenzial +``` + +### 5.2 Phase Classification Engine + +**Composite Score Formel:** +``` +Phase_Score = 0.30 × Normalize(PortShipment_share) + + 0.20 × Normalize(ASP_decline_rate) + + 0.15 × Normalize(Standards_maturity) + + 0.15 × Normalize(InteropValidation_level) + + 0.10 × Normalize(VendorCount_trajectory) + + 0.10 × Normalize(MediaSentiment_score) +``` + +**Phase-Determination (Pseudocode):** +```typescript +function determinePhase(technology: string): HypeCyclePhase { + const shipmentShare = getPortShipmentShare(technology); + const aspDecline = getASPDeclineRate(technology); + const vendorTrend = getVendorCountTrend(technology); + const standardsStatus = getStandardsStatus(technology); + const mediaHype = getMediaHypeIndex(technology); + const interopLevel = getInteropLevel(technology); + + if (shipmentShare < 0.01 && standardsStatus === 'draft') + return 'INNOVATION_TRIGGER'; + + if (shipmentShare < 0.05 && mediaHype > 70 && vendorTrend === 'increasing') + return 'PEAK_OF_INFLATED_EXPECTATIONS'; + + if (aspDecline > 30 && vendorTrend === 'decreasing' && mediaHype < 40) + return 'TROUGH_OF_DISILLUSIONMENT'; + + if (shipmentShare >= 0.05 && shipmentShare <= 0.30 + && aspDecline >= 10 && aspDecline <= 25 + && vendorTrend === 'stable') + return 'SLOPE_OF_ENLIGHTENMENT'; + + if (shipmentShare > 0.30 && aspDecline < 10) + return 'PLATEAU_OF_PRODUCTIVITY'; + + return calculateNearestPhase(compositeScore); +} +``` + +### 5.3 Aktuelle Hype Cycle Positionen (März 2026) + +| Technologie | Phase | Evidenz | +|---|---|---| +| 10G SFP+ | Plateau (Declining) | Commodity, sinkende Shipments, wird verdrängt | +| 40G QSFP+ | Late Plateau / Legacy | Kaum noch neue Deployments | +| 100G QSFP28 | Plateau of Productivity | >30% Marktanteil, Commodity-Preise | +| 400G QSFP-DD | Slope → Plateau Übergang | Breite Adoption, ~33% Revenue mit 800G | +| 800G OSFP/QSFP-DD800 | Peak → Slope Übergang | Explosives Wachstum (3x in Q2'25), AI-getrieben | +| 1.6T | Innovation Trigger | Standards in Entwicklung, erste OFC-Demos | +| CPO (Co-Packaged Optics) | Peak of Inflated Expectations | Viel Presse, wenige Deployments | +| LPO (Linear Pluggable Optics) | Slope of Enlightenment | Erste Produktions-Deployments 2025-26 | +| 400ZR Coherent | Plateau of Productivity | Erfolgreichste kohärente Technologie | +| 800ZR Coherent | Innovation Trigger → Peak | OIF IA veröffentlicht Okt 2024 | + +--- + +## 6. MCP SERVER — TOOL DEFINITIONS + +Der MCP Server macht die gesamte Plattform für LLMs (EO Global Pulse, FAQ Chat) zugänglich: + +```typescript +// Tool Definitions für den MCP Server + +tools: [ + { + name: "search_transceivers", + description: "Suche Transceiver nach Freitext, Specs oder Kompatibilität", + parameters: { + query: "string — Freitext ('10km für Cisco Nexus')", + form_factor: "optional — SFP+, QSFP28, etc.", + speed_gbps: "optional — 10, 100, 400, 800", + reach_label: "optional — SR, LR, ER, ZR", + fiber_type: "optional — SMF, MMF", + wdm_type: "optional — CWDM, DWDM", + vendor: "optional — Vendor-Filter", + max_results: "optional — Default 10" + } + }, + { + name: "check_compatibility", + description: "Prüfe Kompatibilität: Switch + Transceiver", + parameters: { + switch_model: "string — z.B. 'Cisco Nexus 93180YC-FX3'", + transceiver_query: "optional — Transceiver Part Number oder Beschreibung", + speed_gbps: "optional", + reach: "optional" + } + }, + { + name: "get_pricing", + description: "Aktuelle Preise + Verfügbarkeit über alle Quellen", + parameters: { + part_number: "string", + vendor: "optional — nur bestimmten Vendor zeigen", + include_history: "optional — Preis-History über Zeit" + } + }, + { + name: "compare_prices", + description: "Preisvergleich: Flexoptix vs. Wettbewerber", + parameters: { + transceiver_query: "string", + competitors: "optional — Liste spezifischer Wettbewerber" + } + }, + { + name: "get_hype_cycle", + description: "Hype Cycle Status für Technologie/Produkt", + parameters: { + technology: "string — z.B. '800G OSFP', 'CPO', '400ZR'", + include_forecast: "optional — Norton-Bass Prognose" + } + }, + { + name: "search_knowledge_base", + description: "Troubleshooting, FAQs, Best Practices durchsuchen", + parameters: { + query: "string — z.B. 'niedrige Rx Power bei QSFP28'", + category: "optional — troubleshooting, faq, best_practice", + severity: "optional — critical, high, medium, low" + } + }, + { + name: "search_manuals", + description: "Switch-Handbücher und Konfig-Anleitungen durchsuchen", + parameters: { + query: "string — z.B. 'DWDM Konfiguration Juniper MX'", + vendor: "optional", + doc_type: "optional — manual, config_guide, compatibility_list" + } + }, + { + name: "get_templates", + description: "FlexBox Coding Templates oder Switch-Konfig-Templates finden", + parameters: { + type: "flexbox_coding | switch_config", + switch_vendor: "optional", + transceiver_type: "optional", + technology: "optional — CWDM, DWDM, SR, LR" + } + }, + { + name: "suggest_alternatives", + description: "Alternative Transceiver vorschlagen (ähnliche Specs, anderer Vendor/Preis)", + parameters: { + part_number: "string", + optimize_for: "optional — price, availability, performance" + } + }, + { + name: "get_competitor_stock", + description: "Live-Lagerbestand eines Wettbewerbers", + parameters: { + competitor: "string — z.B. 'FS.com'", + product_query: "optional — Filter", + out_of_stock_only: "optional — nur fehlende Produkte (Sales-Chance!)" + } + }, + { + name: "get_market_news", + description: "Aktuelle News und Messe-Berichte", + parameters: { + query: "optional", + event: "optional — OFC, ECOC, CIOE", + days_back: "optional — Default 30" + } + }, + { + name: "generate_blog_draft", + description: "Blog-Entwurf aus Marktdaten generieren", + parameters: { + topic: "hype_cycle | price_trend | new_product | comparison | tutorial", + technology: "optional — z.B. '800G'", + target_audience: "sales | technical | customer | seo" + } + } +] +``` + +--- + +## 7. ANWENDUNGSFÄLLE + +### 7.1 Sales Advisor (Freitext-Suche) + +**User:** "Kunde hat Cisco Nexus 93180YC-FX3 und braucht einen Transceiver für 10km Singlemode" + +**System:** +1. Vector-Suche in `product_embeddings` → Top-5 passende Transceiver +2. `compatibility` Check → Verifiziert für diesen Switch +3. `price_observations` → Aktueller Preis bei Flexoptix + Wettbewerber +4. `knowledge_base` → Bekannte Issues mit diesem Switch/Transceiver + +**Antwort:** "Für den Nexus 93180YC-FX3 empfehle ich den 10G-SFP-LR (10km, SMF, LC). Verifiziert kompatibel. Unser Preis: 39 EUR. FS.com: $12.50. Hinweis: Firmware 10.2(3) oder höher erforderlich." + +### 7.2 Hype Cycle Dashboard + +- Interaktive Visualisierung aller Technologien auf der Gartner-Kurve +- Norton-Bass Prognose: Wann erreicht 800G den Plateau? → ~2029 +- Klickbar: Details pro Technologie (Vendor Count, ASP Trend, Shipment Share) +- Export als Blog-Post oder Präsentation + +### 7.3 Competitor Alert System + +**Trigger:** FS.com Out-of-Stock bei 100G QSFP28 LR4 +**Alert:** "FS.com hat 100G-QSFP28-LR4 nicht auf Lager (seit 2 Stunden). Lead Time: 14 Tage. → Sales-Chance: 47 Kunden haben dieses Produkt in den letzten 90 Tagen bei FS.com gekauft." + +### 7.4 Blog-Generierung + +**Input:** `generate_blog_draft(topic="hype_cycle", technology="800G")` + +**System:** +1. Holt aktuelle Hype Cycle Daten aus `market_metrics` +2. Holt Preis-Trends aus `price_observations` +3. Holt News aus `news_articles` +4. Generiert Draft via Ollama qwen2.5:14b auf .213 +5. Speichert in `blog_drafts` zur Review + +### 7.5 Template Finder + +**User:** "Welchen FlexBox Code brauche ich für einen CWDM 10G auf Juniper EX4300?" + +**System:** +1. `templates` WHERE type='flexbox_coding' AND switch_vendor='Juniper' AND technology='CWDM' +2. `compatibility` Check → Verifiziert +3. `knowledge_base` → Best Practices für CWDM auf Juniper + +--- + +## 8. IMPLEMENTIERUNGS-ROADMAP + +### Phase 0: Foundation (Woche 1-2) +- [ ] PostgreSQL 17 + TimescaleDB auf Erik (.82) installieren +- [ ] Qdrant Docker Container auf Erik starten +- [ ] Schema deployen (alle Core Tables + Hypertables) +- [ ] Bestehende npm DB (159 Transceiver) als Seed-Daten importieren +- [ ] Basis-API (Node.js/Express/PM2) mit CRUD-Endpoints +- [ ] Cloudflare Tunnel einrichten (transceiver-db.context-x.org) + +### Phase 1: Data Ingestion (Woche 3-5) +- [ ] Crawlee Scraper Framework auf Erik einrichten +- [ ] Erster Scraper: FS.com (Preise + Stock + Produkte) +- [ ] Scraper 2-5: Optcore, QSFPTEK, 10Gtek, Edgeium +- [ ] Cisco TMG Matrix Scraper +- [ ] Juniper HCT Scraper +- [ ] Change Detection mit Content Hashing +- [ ] pg-boss Job Scheduler konfigurieren +- [ ] Adaptive Scheduling implementieren + +### Phase 2: Intelligence Layer (Woche 6-8) +- [ ] OCR Pipeline: Docling → Chunk → Embed → Qdrant +- [ ] Erste Transceiver-Datasheets verarbeiten (Top 50) +- [ ] Erste Switch-Handbücher verarbeiten (Cisco, Juniper, Arista) +- [ ] Vector Search Endpoints mit Payload Filtering +- [ ] Full-Text Search via PostgreSQL tsvector +- [ ] Knowledge Base: FAQ-Scraping starten (FS.com, Cisco, FluxLight) +- [ ] MCP Server (Basis-Tools) + +### Phase 3: Hype Cycle Engine (Woche 9-10) +- [ ] Norton-Bass Model implementieren +- [ ] Historische Daten laden (Port Shipments, ASP, Vendor Count) +- [ ] Phase Classification Engine +- [ ] Media Sentiment Tracking (Google Trends + News) +- [ ] Hype Cycle Visualisierung +- [ ] Erste Prognosen generieren + +### Phase 4: Scale Crawlers (Woche 11-14) +- [ ] Alle 60+ identifizierten Quellen anbinden +- [ ] Chinesische Hersteller-Websites (InnoLight, Eoptolink, HGGenuine...) +- [ ] SE-Asien Factory Monitoring +- [ ] News Aggregation (Lightwave, Light Reading, optics.org...) +- [ ] Messe-Coverage (OFC, ECOC Archive) +- [ ] Proxy-Rotation einrichten (Bright Data / Geonode) +- [ ] Alert System implementieren + +### Phase 5: Customer-Facing (Woche 15-18) +- [ ] Cloudflare Workers API Gateway +- [ ] FAQ Chatbot (RAG über Knowledge Base) +- [ ] EO Global Pulse Integration +- [ ] Sales Advisor Tool +- [ ] Template Finder +- [ ] Price Comparison Dashboard +- [ ] Blog Draft Generator +- [ ] Multi-Tenant Access Control + +### Phase 6: Domination (Woche 19+) +- [ ] Alle 400+ Switch-Hersteller (Flexoptix Vendor Page) als Daten +- [ ] Alle Datasheets aller Transceiver verarbeitet +- [ ] Alle Switch-Handbücher digitalisiert +- [ ] Vollständige Kompatibilitätsmatrix +- [ ] Automatische Blog-Veröffentlichung +- [ ] API für externe Partner +- [ ] Mobile App (EO Global Pulse) + +--- + +## 9. KRITISCHE ERFOLGSFAKTOREN + +### 9.1 Rechtliche Aspekte Scraping +- `robots.txt` immer respektieren +- Rate-Limiting: Max 1-2 Requests/Sekunde pro Domain +- Legitimer User-Agent +- Kein Scraping hinter Authentication (ohne Account) +- Aggressiv cachen — keine unveränderten Seiten erneut abfragen +- Offizielle APIs bevorzugen wo verfügbar (Nexar/Octopart, Mouser, DigiKey) + +### 9.2 Datenqualität +- Jeder Datenpunkt braucht `source_url` und `last_verified` +- Automatische Anomalie-Detection bei Preissprüngen (>50% Änderung → Review) +- Community-Verifizierung für Kompatibilität +- Regelmäßige Stichproben durch Flexoptix-Team + +### 9.3 Markt-Chance +**Es gibt KEINEN Octopart/FindChips-Äquivalent für optische Transceiver-Module.** +Kein Preisvergleichs-Aggregator. Keine öffentliche API die mehrere Optical-Transceiver-Vendor abdeckt. + +**TIP könnte dieses Vakuum füllen.** + +--- + +## 10. BESTEHENDE DATEN-INTEGRATION + +Das aktuelle npm Package `transceiver-db` (159 Transceiver, 39 Standards, 16 Form Factors) wird als **Seed-Daten** importiert: + +```typescript +// Migration: npm DB → PostgreSQL +import { getAllTransceivers, getAllStandards } from 'transceiver-db'; + +const transceivers = getAllTransceivers(); +const standards = getAllStandards(); + +// Insert into PostgreSQL with vendor_id mapping +for (const t of transceivers) { + await db.query(` + INSERT INTO transceivers (part_number, standard, form_factor, speed_gbps, ...) + VALUES ($1, $2, $3, $4, ...) + `, [t.id, t.standard, t.formFactor, t.speedGbps, ...]); +} +``` + +Das npm Package bleibt als **Open-Source Lightweight-Version** auf GitHub bestehen. +TIP wird die **Enterprise Intelligence Layer** darüber. + +--- + +## ZUSAMMENFASSUNG + +| Dimension | Scope | +|-----------|-------| +| **Transceiver** | Alle Generationen (1G SFP → 1.6T), CWDM, DWDM, Coherent, AOC, DAC, Breakout | +| **Switches** | 400+ Hersteller, alle Serien, Specs, Lifecycle, ASIC Info | +| **Kompatibilität** | Switch ↔ Transceiver Matrix, verifiziert + Community | +| **Preise** | Echtzeit von 60+ Quellen weltweit | +| **Stock** | Live-Lagerbestände aller Wettbewerber | +| **Handbücher** | Auto-Crawl, OCR, MCP-durchsuchbar | +| **Datasheets** | Alle technischen Specs aller Transceiver | +| **FAQs** | Gescannt von allen Vendor-Portalen | +| **Templates** | FlexBox Coding + Switch-Konfiguration | +| **Hype Cycle** | Norton-Bass basiert, automatisch aktualisiert | +| **News** | OFC, ECOC, CIOE, Photonics West, Trade Press | +| **Fabriken** | Alle Fertigungsstätten weltweit | +| **Blog** | Auto-generiert aus Marktdaten | +| **Alerts** | Out-of-Stock, Preisänderungen, neue Produkte | + +**Es gibt nichts Vergleichbares auf dem Markt. Das wird das Referenzwerk der Optical Networking Industrie.** diff --git a/RESEARCH-optical-transceiver-history.md b/RESEARCH-optical-transceiver-history.md new file mode 100644 index 0000000..621ae78 --- /dev/null +++ b/RESEARCH-optical-transceiver-history.md @@ -0,0 +1,672 @@ +# Optical Transceiver Evolution: Complete History & Database Reference (2001-2026) + +> Deep research compiled from OFC proceedings, LightCounting, Cignal AI, IEEE, OIF, and industry publications. +> Last updated: 2026-03-27 + +--- + +## Table of Contents + +1. [Form Factor Evolution Timeline](#1-form-factor-evolution-timeline) +2. [Speed Tier Evolution](#2-speed-tier-evolution) +3. [Key Standards & Adoption Timelines](#3-key-standards--adoption-timelines) +4. [CWDM vs DWDM Evolution](#4-cwdm-vs-dwdm-evolution) +5. [Major Transceiver Manufacturers](#5-major-transceiver-manufacturers) +6. [Next-Generation Technologies (2025-2030)](#6-next-generation-technologies-2025-2030) +7. [Market Data Points](#7-market-data-points) +8. [Database Schema Recommendations](#8-database-schema-recommendations) +9. [Hype Cycle Analysis](#9-hype-cycle-analysis) + +--- + +## 1. Form Factor Evolution Timeline + +### Complete Form Factor Database + +| Form Factor | Year Introduced | Peak Adoption | Legacy/Decline | Max Speed | Connector | Lanes | Status | +|---|---|---|---|---|---|---|---| +| **GBIC** | 1995 | 2000-2004 | 2006+ | 2.5 Gbps | SC Duplex | 1 | Obsolete | +| **SFP** | 2001 | 2004-present | Still active (1G) | 4.25 Gbps | LC Duplex | 1 | Active (legacy speeds) | +| **XENPAK** | 2001 | 2002-2006 | 2007+ | 10 Gbps | SC Duplex | 1 | Obsolete | +| **X2** | 2003 | 2004-2008 | 2009+ | 10 Gbps | SC Duplex | 1 | Obsolete | +| **XFP** | 2002 (MSA), 2003 (adopted) | 2005-2012 | 2013+ | 10 Gbps (DWDM capable) | LC Duplex | 1 | Legacy | +| **SFP+** | 2006 | 2008-present | Still active | 16 Gbps | LC Duplex | 1 | Active | +| **QSFP** | 2006 | 2008-2012 | 2013+ | 4x1G = 4 Gbps | MPO-12 | 4 | Legacy | +| **CFP** | 2009 | 2010-2016 | 2017+ | 100 Gbps | LC Duplex/MPO | 10x10G | Legacy | +| **QSFP+** | 2012 | 2013-2020 | Declining | 40 Gbps | MPO-12 / LC | 4x10G | Active (declining) | +| **CFP2** | 2012 | 2014-2020 | 2021+ | 200 Gbps | LC Duplex | varies | Legacy (except coherent) | +| **CFP4** | 2014 | 2015-2019 | 2020+ | 100 Gbps | LC Duplex | 4x25G | Legacy | +| **QSFP28** | 2014 | 2016-2023 | Declining | 100 Gbps | LC / MPO-12 | 4x25G | Active (declining) | +| **SFP28** | 2014 | 2016-present | Still active | 25 Gbps | LC Duplex | 1 | Active | +| **OSFP** | 2016 (announced) | 2020-present | - | 800 Gbps (8x100G) | MPO-16 / LC | 8 | Active (growing) | +| **CSFP** | 2018 | 2019-present | - | 2x1 Gbps | LC (BiDi) | 2 (BiDi) | Niche | +| **QSFP56** | 2019 | 2020-2024 | Declining | 200 Gbps | MPO-12 / LC | 4x50G | Active (declining) | +| **QSFP-DD** | 2019 | 2021-present | - | 800 Gbps (8x100G) | MPO-16 / LC | 8 | Active (growing) | +| **SFP56** | 2020 (spec), 2024 (products) | 2024-present | - | 50 Gbps | LC Duplex | 1 | Active (emerging) | +| **QSFP112** | 2021 | 2022-present | - | 400 Gbps | MPO-12 / LC | 4x100G | Active | +| **SFP-DD** | 2017 (spec) | 2020-present | - | 2x25G = 50 Gbps | LC Duplex | 2 | Niche | +| **OSFP-XD** | 2022 | 2025-present | - | 1.6T (16x100G), 3.2T future | MPO-16 | 16 | Emerging | +| **QSFP-DD1600** | 2024 (spec in progress) | 2026+ (projected) | - | 1.6T (8x200G) | MPO-16 | 8 | Emerging | +| **OSFP1600** | 2022 (spec) | 2025-2026 | - | 1.6T (8x200G) | MPO-16 | 8 | Emerging | + +### Form Factor Hype Cycle Phases + +``` +Phase 1: INTRODUCTION - Standard published, first samples +Phase 2: EARLY ADOPTION - Hyperscale/cloud first movers +Phase 3: MAINSTREAM - Broad enterprise deployment, pricing declines +Phase 4: MATURITY - Commoditized, price floor reached +Phase 5: DECLINE - Next generation overtakes, volume drops +Phase 6: LEGACY - Minimal new deployments, maintenance only +Phase 7: OBSOLETE - No longer manufactured +``` + +| Form Factor | Current Phase (2026) | +|---|---| +| GBIC | 7-OBSOLETE | +| XENPAK | 7-OBSOLETE | +| X2 | 7-OBSOLETE | +| XFP | 6-LEGACY | +| SFP (1G) | 4-MATURITY | +| SFP+ (10G) | 4-MATURITY | +| QSFP+ (40G) | 5-DECLINE | +| CFP/CFP2/CFP4 | 6-LEGACY (except CFP2-DCO) | +| SFP28 (25G) | 3-MAINSTREAM | +| QSFP28 (100G) | 4-MATURITY / 5-DECLINE | +| QSFP56 (200G) | 5-DECLINE | +| QSFP-DD (400G/800G) | 3-MAINSTREAM | +| OSFP (400G/800G) | 3-MAINSTREAM | +| QSFP112 (400G) | 2-EARLY ADOPTION | +| OSFP-XD (1.6T) | 1-INTRODUCTION | +| QSFP-DD1600 (1.6T) | 1-INTRODUCTION | + +--- + +## 2. Speed Tier Evolution + +### Speed Tier Database + +| Speed | Year Standardized | Year Mainstream | Dominant Form Factor | Modulation | Lanes | Key Standard | Current Status | +|---|---|---|---|---|---|---|---| +| **1G** | 1998 (802.3z) | 2002 | SFP | NRZ | 1 | IEEE 802.3z | Mature/commodity | +| **10G** | 2002 (802.3ae) | 2007 | SFP+ | NRZ | 1 | IEEE 802.3ae | Mature/commodity | +| **25G** | 2016 (802.3by) | 2018 | SFP28 | NRZ | 1 | IEEE 802.3by | Mainstream | +| **40G** | 2010 (802.3ba) | 2013 | QSFP+ | NRZ | 4x10G | IEEE 802.3ba | Declining | +| **50G** | 2016 (802.3cd) | 2020 | SFP56 / QSFP28 | PAM4 (single lane) | 1 | IEEE 802.3cd | Niche | +| **100G** | 2010 (802.3ba) / 2014 (QSFP28) | 2017 | QSFP28 | NRZ (4x25G) | 4 | IEEE 802.3ba | Mainstream/declining | +| **200G** | 2017 (802.3bs) | 2020 | QSFP56 / QSFP-DD | PAM4 | 4x50G | IEEE 802.3bs | Active | +| **400G** | 2017 (802.3bs) | 2022 | QSFP-DD / OSFP | PAM4 | 8x50G or 4x100G | IEEE 802.3bs | Mainstream | +| **800G** | 2024 (802.3df) | 2024-2025 | OSFP / QSFP-DD | PAM4 | 8x100G | IEEE 802.3df | Rapid growth | +| **1.6T** | 2026 (802.3dj target) | 2026-2027 (projected) | OSFP-XD / OSFP1600 | PAM4 | 8x200G or 16x100G | IEEE 802.3dj | Emerging | + +### Speed Tier Adoption S-Curves (Port Shipment Peak Years) + +``` +1G: Peak ~2010-2014, still shipping in volume for enterprise access +10G: Peak ~2016-2020, declining but high volume +25G: Peak ~2020-2024, server-side standard +40G: Peak ~2015-2019, largely replaced by 100G +100G: Peak ~2020-2024, transitioning to 400G +400G: Peak ~2024-2027 (projected), current mainstream for spine/core +800G: Peak ~2026-2029 (projected), AI backend standard +1.6T: Peak ~2028-2031 (projected), next-gen AI/HPC +``` + +### Modulation Technology Timeline + +| Technology | Speed Range | Years Active | Key Characteristic | +|---|---|---|---| +| NRZ (Non-Return-to-Zero) | 1G-25G per lane | 1995-present | 1 bit per symbol, simple | +| PAM4 (4-level Pulse Amplitude) | 50G-200G per lane | 2017-present | 2 bits per symbol, requires DSP/FEC | +| Coherent (DP-QPSK/DP-16QAM) | 100G-800G per wavelength | 2011-present | Phase + amplitude, long-haul | + +### Per-Lane Rate Evolution + +| Year | Per-Lane Rate | Technology | Key Enabler | +|---|---|---|---| +| 2001-2005 | 1G | NRZ | DFB/VCSEL | +| 2006-2013 | 10G | NRZ | DFB/VCSEL, CDR | +| 2014-2018 | 25G | NRZ | EML, CDR | +| 2019-2022 | 50G | PAM4 | DSP (7nm/5nm) | +| 2022-2025 | 100G | PAM4 | DSP (5nm/3nm), SiPh | +| 2025-2028 | 200G | PAM4 | DSP (3nm), advanced FEC | + +--- + +## 3. Key Standards & Adoption Timelines + +### IEEE 802.3 Optical Ethernet Standards + +| Standard | Year Ratified | Speed | Key PHY Types | Notes | +|---|---|---|---|---| +| 802.3z | 1998 | 1 Gbps | 1000BASE-SX, 1000BASE-LX | First Gigabit Ethernet | +| 802.3ae | June 2002 | 10 Gbps | 10GBASE-SR, -LR, -ER, -LX4 | First 10GbE, fiber only | +| 802.3aq | 2006 | 10 Gbps | 10GBASE-LRM | Long reach multimode | +| 802.3ba | June 2010 | 40/100 Gbps | 40GBASE-SR4/LR4, 100GBASE-SR10/LR4/ER4 | First multi-rate standard | +| 802.3bm | 2015 | 40/100 Gbps | 40GBASE-SR4 (OM3/OM4), 100GBASE-SR4 | Improved MMF reach | +| 802.3by | 2016 | 25 Gbps | 25GBASE-SR, 25GBASE-LR | Single-lane 25G | +| 802.3bs | Dec 2017 | 200/400 Gbps | 200GBASE-DR4, 400GBASE-SR16/DR4/FR8/LR8 | First PAM4 in standard | +| 802.3cd | Dec 2018 | 50/100/200 Gbps | 50GBASE-SR/LR/FR/CR, 100GBASE-DR/SR2 | Single-lane 50G NRZ | +| 802.3cm | 2020 | 400 Gbps | 400GBASE-SR4.2 | Short-reach MMF (BiDi SWDM) | +| 802.3ct | 2021 | 100 Gbps | 100GBASE-ZR | Coherent 100G pluggable | +| 802.3cu | 2021 | 100/400 Gbps | 100GBASE-FR1/LR1, 400GBASE-FR4 | Single-lambda 100G | +| 802.3ck | Sep 2022 | 100/200/400 Gbps | Electrical interfaces (100G/lane) | Defines 100G SerDes | +| 802.3db | Sep 2022 | 100/200/400 Gbps | 100GBASE-VR1, 400GBASE-VR4 | Very short reach | +| 802.3df | Feb 2024 | 400/800 Gbps | 800GBASE-DR8, 400GBASE-DR4-2 | 800G standard | +| 802.3dj | ~2026 (target) | 200/400/800/1600 Gbps | 200G/lane PHYs | 1.6T Ethernet | + +### OIF Implementation Agreements + +| Agreement | Year Published | Speed | Max Reach | Key Feature | +|---|---|---|---|---| +| VSR-5 OIF-05.0 | ~2010 | 100G | 100m | Very short reach coherent | +| 400ZR | Dec 2020 | 400G | 120km (amplified) | Pluggable coherent DWDM in QSFP-DD/OSFP | +| 400ZR+ (vendor-specific) | 2021 | 400G | 450-600km | Extended reach, oFEC | +| 800ZR (in progress) | 2024-2025 | 800G | 80-120km | Next-gen pluggable coherent | +| 1600ZR (in progress) | 2025+ | 1.6T | TBD | Future coherent standard | +| CEI-112G | 2021 | 112 Gbps/lane | Chip-to-module | 100G PAM4 electrical interface | +| CEI-224G | 2025 (target) | 224 Gbps/lane | Chip-to-module | 200G PAM4 electrical interface | + +### Multi-Source Agreements (MSAs) + +| MSA | Year Published | Speed | Technology | Reach | Key Members | +|---|---|---|---|---|---| +| SFP MSA | 2000 | 1-4G | Various | Varies | Finisar, JDS, Agilent | +| XFP MSA | 2002 | 10G | Various | Varies | Finisar + 10 companies | +| SFP+ MSA (SFF-8431) | 2006 | 10G | NRZ | Varies | Industry-wide | +| QSFP+ MSA (SFF-8436) | 2009 | 40G | 4x10G NRZ | Varies | Industry-wide | +| CFP MSA | 2009 | 100G | 10x10G/4x25G | Varies | Industry-wide | +| QSFP28 MSA (SFF-8665) | 2014 | 100G | 4x25G NRZ | Varies | Industry-wide | +| 100G PSM4 MSA | Mar 2014 | 100G | 4x25G parallel SM | 500m | Corning, Intel, Luxtera, etc. | +| 100G CWDM4 MSA | Sep 2014 | 100G | 4x25G CWDM | 2km | Avago, Finisar, JDSU, etc. | +| SFP28 MSA (SFF-8402) | 2014 | 25G | NRZ | Varies | Industry-wide | +| 25G Ethernet Consortium | 2014 | 25/50G | NRZ | Varies | Arista, Broadcom, Google, Microsoft | +| 100G Lambda MSA | Sep 2017 | 100G/400G | Single-lambda 100G PAM4 | 2-40km | Alibaba, Cisco, Intel, +39 members | +| QSFP-DD MSA | 2017 | 200-800G | 8-lane double density | Varies | Broadcom, Cisco, Finisar, etc. | +| OSFP MSA | 2016 | 400-800G | 8-lane octal | Varies | Arista, Broadcom, Mellanox, etc. | +| OpenZR+ MSA | May 2020 | 100-400G | Coherent DWDM | 1000+km | Acacia, Cisco, Juniper, Lumentum | +| OSFP-XD MSA | 2022 | 1.6-3.2T | 16-lane | Varies | Industry-wide | +| CMIS (Common Mgmt Interface) | v5.0: 2020, v5.3: 2024 | All | Management spec | - | Industry-wide | + +--- + +## 4. CWDM vs DWDM Evolution + +### CWDM Technical Specifications + +| Parameter | Value | +|---|---| +| Standard | ITU-T G.694.2 | +| Wavelength Range | 1270-1610 nm | +| Channel Spacing | 20 nm | +| Total Channels | 18 (full grid) | +| Practical Channels | 8-16 (water peak limits 1370-1410nm) | +| Laser Type | Uncooled DFB | +| Max Reach | ~70 km (unamplified) | +| Max Per-Channel Speed | 100 Gbps (current), 25G most common | +| Amplification | None (passive) | +| Cost | Lower (uncooled lasers, wider tolerance) | + +#### CWDM Wavelength Grid + +| Channel | Wavelength (nm) | Band | Notes | +|---|---|---|---| +| 1 | 1271 | O-band | Commonly used | +| 2 | 1291 | O-band | Commonly used | +| 3 | 1311 | O-band | Commonly used | +| 4 | 1331 | O-band | Commonly used | +| 5 | 1351 | E-band | Water peak region | +| 6 | 1371 | E-band | Water peak region | +| 7 | 1391 | S-band | Water peak region (limited to 40km) | +| 8 | 1411 | S-band | Water peak region (limited to 40km) | +| 9 | 1431 | S-band | | +| 10 | 1451 | S-band | | +| 11 | 1471 | C-band edge | Commonly used | +| 12 | 1491 | S/C-band | Commonly used | +| 13 | 1511 | C-band | Commonly used | +| 14 | 1531 | C-band | Commonly used | +| 15 | 1551 | C-band | Commonly used | +| 16 | 1571 | L-band | Commonly used | +| 17 | 1591 | L-band | | +| 18 | 1611 | L-band | | + +### DWDM Technical Specifications + +| Parameter | Value | +|---|---| +| Standard | ITU-T G.694.1 | +| C-Band Range | 1528.77-1563.86 nm (191.7-196.1 THz) | +| L-Band Range | 1565-1625 nm | +| Channel Spacing (100 GHz) | 0.8 nm, ~40 channels in C-band | +| Channel Spacing (50 GHz) | 0.4 nm, ~80 channels in C-band | +| Channel Spacing (25 GHz) | 0.2 nm, ~160 channels (flex grid) | +| Laser Type | Cooled DFB / Tunable | +| Max Reach | 3000+ km (amplified with EDFA/Raman) | +| Max Per-Channel Speed | 800 Gbps (coherent pluggable) | +| Amplification | EDFA, Raman | +| Flex Grid | Supports variable channel widths (12.5 GHz granularity) | + +### Coherent Optics Evolution + +| Generation | Year | Per-Wavelength Rate | Modulation | Baud Rate | Form Factor | +|---|---|---|---|---|---| +| Gen 1 | 2011 | 40G | DP-QPSK | 10-12 GBd | Line card (chassis) | +| Gen 2 | 2012 | 100G | DP-QPSK | 32 GBd | Line card / CFP | +| Gen 3 | 2016 | 200G | DP-16QAM | 32-45 GBd | CFP2-DCO | +| Gen 4 | 2018 | 400G | DP-16QAM | 64 GBd | CFP2-DCO | +| Gen 5 (400ZR) | 2021 | 400G | DP-16QAM | 60 GBd | QSFP-DD / OSFP | +| Gen 6 (ZR+) | 2022 | 400G | DP-16QAM (enhanced) | 64 GBd | QSFP-DD / OSFP | +| Gen 7 (800ZR) | 2024 | 800G | DP-64QAM / prob-shaped | 100+ GBd | QSFP-DD / OSFP | +| Gen 8 (1600ZR) | 2026+ | 1.6T | TBD | 130+ GBd | OSFP / OSFP-XD | + +### C+L Band Capacity Evolution + +| Year | Typical System Capacity | Technology | +|---|---|---| +| 2005 | 40x10G = 400 Gbps | C-band, 100GHz grid | +| 2010 | 80x40G = 3.2 Tbps | C-band, 50GHz grid | +| 2015 | 80x100G = 8 Tbps | C-band, 50GHz grid, coherent | +| 2020 | 80x400G = 32 Tbps | C-band, flex grid | +| 2024 | 80x800G = 64 Tbps | C-band, flex grid | +| 2025+ | 120+x800G = 96+ Tbps | C+L band, flex grid | + +### Key WDM Transceiver Types by Speed + +| Speed | CWDM Variants | DWDM Variants | +|---|---|---| +| 1G | SFP CWDM (18 wavelengths) | SFP DWDM (C-band) | +| 10G | SFP+ CWDM, XFP CWDM | XFP/SFP+ DWDM (tunable) | +| 25G | SFP28 CWDM | SFP28 DWDM | +| 40G | QSFP+ CWDM4 | CFP DWDM (coherent) | +| 100G | QSFP28 CWDM4 | QSFP28 DWDM / CFP2-DCO | +| 400G | (not practical) | QSFP-DD/OSFP ZR/ZR+ | +| 800G | (not practical) | OSFP/QSFP-DD 800ZR/ZR+ | + +--- + +## 5. Major Transceiver Manufacturers + +### Manufacturer Database + +| Company | HQ | Founding | Key Milestones | Specialty | 2024 Revenue (transceivers) | Market Position | +|---|---|---|---|---|---|---| +| **Coherent Corp.** | Pittsburgh, USA | 1971 (as II-VI) | Acquired Finisar ($3.2B, 2019), Coherent ($6.56B, 2022) | Coherent, Datacom, InP lasers | ~$2.5B+ | #2 globally, #1 telecom | +| **Zhongji Innolight** | Suzhou, China | 2008 | #1 globally 2023, 50%+ Nvidia wallet share | Datacom, 800G/1.6T | ~$3.3B (114% YoY growth) | #1 globally | +| **Lumentum** | San Jose, USA | 2015 (spun off JDS Uniphase) | Acquired Cloud Light ($750M, 2024), Oclaro ($1.8B, 2018) | Coherent, lasers, 3D sensing | ~$1.5B | #3 globally | +| **Broadcom (Optical)** | San Jose, USA | Broadcom acquired original Avago/LSI/Broadcom | Key DSP/PAM4 supplier | DSP chips, SiPh, VCSEL | ~$1B+ | Major component supplier | +| **Cisco (Silicon Photonics)** | San Jose, USA | Acquired Luxtera ($660M, 2019), Acacia ($4.6B, 2021) | Integrated SiPh transceivers | SiPh, coherent (via Acacia) | Internal consumption + merchant | #4-5 globally | +| **Eoptolink** | Shenzhen, China | 2004 | 175% revenue growth 2024, #3 globally | Datacom, LPO, SiPh | ~$1.2B | #3 globally | +| **HG Genuine** | Wuhan, China | 2001 | ByteDance/TikTok supplier since 2021 | Datacom, access optics | ~$600M+ | #8 globally | +| **Accelink Technologies** | Wuhan, China | 2001 | Chinese cloud supplier | Telecom, passive components | ~$600M+ | #5 globally | +| **Hisense Broadband** | Qingdao, China | 2003 (Hisense subsidiary) | PON/access market leader | Access, PON, 5G | ~$600M+ | #6 globally | +| **Source Photonics** | West Hills, USA / China | 2002 | Chinese cloud supplier | Access, enterprise, DC | ~$400M | #9 globally | +| **Applied Optoelectronics (AOI)** | Sugar Land, USA | 1997 | CATV and DC optics | VCSEL, DFB, DC transceivers | ~$200M | Niche | +| **Intel Silicon Photonics** | Santa Clara, USA | SiPh division ~2010 | 100G PSM4, 1.6T SiPh engines | Silicon photonics platform | Sold to third parties (Jabil etc.) | Technology leader | +| **ColorChip** | Yokneam, Israel | 2001 | Acquired by Source Photonics 2018 | PLC-based transceivers | (merged) | Acquired | +| **Broadex Technologies** | Chengdu, China | 2016 | Fast-growing Chinese supplier | Datacom, 400G/800G | ~$300M | Emerging | +| **Centera Photonics** | Taiwan | 2007 | 800G/1.6T development | Datacom transceivers | ~$150M | Regional | + +### Market Share Trends (Global Optical Transceiver Revenue) + +| Year | #1 | #2 | #3 | Chinese in Top 10 | Key Shift | +|---|---|---|---|---|---| +| 2015 | Finisar | Lumentum/JDSU | Avago/Broadcom | 2-3 | US/Japan dominance | +| 2018 | Finisar | II-VI | Lumentum | 3-4 | Pre-merger era | +| 2020 | Coherent (II-VI+Finisar) | Innolight | Lumentum | 4-5 | Chinese rise begins | +| 2022 | Innolight = Coherent (~$1.4B each) | Lumentum | Accelink | 5-6 | Chinese parity | +| 2023 | Innolight | Coherent | Lumentum | 7 of top 10 | Chinese dominance | +| 2024 | Innolight ($3.3B) | Coherent (~$2.5B) | Eoptolink ($1.2B) | 7 of top 10 | AI-driven surge | + +### Major M&A Timeline + +| Year | Acquirer | Target | Value | Impact | +|---|---|---|---|---| +| 2013 | Oclaro | Opnext | $180M | Combined coherent portfolio | +| 2015 | Lumentum spins off | from JDS Uniphase | - | Created independent photonics leader | +| 2018 | II-VI | Finisar | $3.2B | Created #1 transceiver company | +| 2018 | Lumentum | Oclaro | $1.8B | Strengthened InP/coherent capabilities | +| 2019 | Cisco | Luxtera | $660M | Silicon photonics integration | +| 2019 | Cisco | Acacia Communications | $4.6B | Coherent DSP leadership | +| 2021 | Intel | (SiPh division established) | Internal | 100G-1.6T silicon photonics engines | +| 2022 | II-VI | Coherent Inc. (laser co.) | $6.56B | Renamed to Coherent Corp. | +| 2024 | Lumentum | Cloud Light Technology | $750M | DC infrastructure boost | +| 2024 | Nvidia | (investing in optical supply chain) | Various | Vertical integration signal | + +--- + +## 6. Next-Generation Technologies (2025-2030) + +### 1.6T Transceivers + +| Parameter | Gen1 (16x100G) | Gen2 (8x200G) | +|---|---|---| +| Timeline | 2025 (shipping) | 2026 (maturing) | +| Lane Rate | 100G PAM4 | 200G PAM4 | +| Lane Count | 16 | 8 | +| Form Factor | OSFP-XD | OSFP1600, OSFP, QSFP-DD1600 | +| DSP Process | 5nm | 3nm | +| Power (retimed) | ~25-30W | ~17-26W | +| Power (LPO) | ~8-12W | ~5W | +| Key DSPs | Broadcom Sian2, Marvell Aries | Broadcom Sian3, Marvell next-gen | + +### Co-Packaged Optics (CPO) Timeline + +| Year | Milestone | +|---|---| +| 2021 | Broadcom Tomahawk 4 + Humboldt = first CPO chipset | +| 2022 | Broadcom Tomahawk 5 + Bailly = first volume-production CPO | +| 2025 Q1 | NVIDIA announces first 1.6T CPO system (Micro Ring Modulators) | +| 2025 Q2 | NVIDIA Quantum-X SiPh switch ships | +| 2025 | TSMC COUPE platform adopted by NVIDIA, Broadcom | +| 2025 | Meta tests Broadcom CPO for 1M+ link-hours | +| 2025 Nov | Ayar Labs integrates TeraPHY into GUC ASIC workflow | +| 2026 H2 | NVIDIA Spectrum-X Photonics system ships | +| 2026-2027 | Broad CPO commercialization begins | +| 2028-2030 | Large-scale CPO deployment in hyperscale | + +### CPO vs LPO vs Traditional DSP Comparison + +| Feature | Traditional (DSP) | LPO (Linear Drive) | CPO (Co-Packaged) | +|---|---|---|---| +| Power Consumption | Baseline | -30 to -50% | -50 to -84% | +| Latency | ~100ns (DSP) | <15ns reduction | Near-zero electrical path | +| Serviceability | Hot-swappable | Hot-swappable | Requires board replacement | +| Maturity | Production | Shipping (NVIDIA, Meta) | Pre-production/early access | +| Cost | Baseline | Lower (no DSP in module) | Higher initially, lower at scale | +| Best For | Long reach, interop | Short reach (<2km), AI clusters | Ultra-dense, scale-up AI | +| Market Share (2025) | ~60% of 800G/1.6T | ~30% | ~5% | +| Market Share (2030, projected) | ~30% | ~40% | ~30% | + +### Silicon Photonics Adoption + +| Year | SiPh Share of Transceivers | Key Driver | +|---|---|---| +| 2018 | ~14% | 100G PSM4 (Intel) | +| 2020 | ~20% | 400G DR4 ramp | +| 2022 | ~25% | 400G mainstream | +| 2024 | ~35% | 800G ramp | +| 2025 | ~40-45% | 800G mainstream, 1.6T intro | +| 2030 (proj.) | ~60% | LPO + CPO adoption | + +### O-Band vs C-Band Data Center Trends + +| Parameter | O-Band (1310nm) | C-Band (1550nm) | +|---|---|---| +| Primary Use | Data center interconnect (<10km) | Metro/long-haul, DCI (>10km) | +| Technology | Direct detect, PAM4 | Coherent or PAM4 WDM | +| Standards | DR, FR, LR variants | ZR, ZR+, DWDM | +| Advantages | Lower cost, simpler, lower dispersion | Higher capacity, longer reach | +| Trend | Dominant for intra-DC | Growing for inter-DC via ZR/ZR+ | +| 800G Example | 800G-DR8 (O-band, 500m) | 800ZR (C-band, 120km) | + +--- + +## 7. Market Data Points + +### Global Optical Transceiver Market Size + +| Year | Market Size (USD) | YoY Growth | Key Driver | +|---|---|---|---| +| 2019 | ~$6.5B | - | 100G mainstream | +| 2020 | ~$7.0B | +8% | COVID + cloud demand | +| 2021 | ~$8.0B | +14% | 400G ramp begins | +| 2022 | ~$9.0B | +13% | 400G mainstream deployment | +| 2023 | ~$10.5B | +17% | AI infrastructure begins | +| 2024 | ~$13.6B | +30% | AI explosion, 800G ramp | +| 2025 (est.) | ~$15.6-16B | +15-18% | 800G mainstream, 1.6T intro | +| 2029 (proj.) | ~$25B | CAGR 13% | 1.6T mainstream | +| 2034 (proj.) | ~$46B | CAGR 17% | CPO + next-gen | + +### Port Shipment Data + +| Metric | 2023 | 2024 | 2025 (est.) | +|---|---|---|---| +| Total transceiver units deployed | ~15M | ~22.5M | ~34.5M | +| 400G+800G unit shipments | ~6M | 20M+ | 30M+ (est.) | +| Quarterly record (400/800G) | <3M | 5M+ (Q2 2024) | 7M+ (projected) | +| 400G/800G YoY growth | - | +250% | +60% (800G specifically) | +| 800G as % of high-speed | ~20% | ~35% | ~50% (est.) | + +### Average Selling Price (ASP) Trends + +| Speed | Launch ASP | 2024 ASP | ASP Decline Pattern | +|---|---|---|---| +| 1G SFP | ~$500 (2001) | ~$5-15 | >95% decline over 20 years | +| 10G SFP+ | ~$500 (2007) | ~$15-40 | >90% decline over 15 years | +| 25G SFP28 | ~$100 (2016) | ~$15-30 | ~75% decline over 8 years | +| 40G QSFP+ | ~$300 (2012) | ~$30-80 | ~80% decline over 12 years | +| 100G QSFP28 | ~$1,000 (2015) | ~$50-120 | ~90% decline, 60% in last 5 years | +| 400G QSFP-DD | ~$800-1,200 (2020) | ~$120-250 | ~75% decline, SR8 50% in 1 year | +| 800G OSFP | ~$800-1,000 (2023) | ~$360-450 | Early decline, still premium | +| 1.6T OSFP-XD | ~$1,300-1,500 (2025) | $1,300-1,500 | Launch pricing, projected ~$1,100 by 2027 | + +### ASP Decline Model (Typical Pattern) + +``` +Year 0 (Launch): 100% (premium pricing) +Year 1: 80-90% (early adoption) +Year 2: 60-70% (volume ramp) +Year 3: 40-50% (mainstream) +Year 4: 30-40% (commoditization begins) +Year 5+: 20-30% (commodity, Chinese competition) +Year 7+: 10-15% (floor pricing) +``` + +### Market Segment Split (2024-2025) + +| Segment | 2024 Share | 2025 Share (est.) | Growth Driver | +|---|---|---|---| +| Data Centers | 45-55% | 55-60% | AI/ML clusters, hyperscale | +| Telecommunications | 30-40% | 25-30% | 5G, coherent metro/long-haul | +| Enterprise Networking | 14-20% | 12-15% | LAN/WAN upgrades to 100G | +| Other (defense, govt, research) | 5-10% | 5-8% | Specialty applications | + +### Datacom vs Telecom Module Revenue + +| Year | Datacom Revenue | Telecom Revenue | Datacom Share | +|---|---|---|---| +| 2020 | ~$4B | ~$3B | 57% | +| 2022 | ~$5.5B | ~$3.5B | 61% | +| 2024 | ~$9B+ | ~$4B | 69% | +| 2025 (est.) | ~$12B+ | ~$4B | 75% | + +### Coherent Pluggable Shipments + +| Year | 400ZR/ZR+ Units | Key Milestone | +|---|---|---| +| 2021 | <50K | First GA shipments | +| 2022 | ~100-150K | Initial ramp | +| 2023 | ~300K | Broad deployment | +| 2024 | ~500K | Fastest adopted coherent ever | +| 2025 (est.) | ~600K+ | 800ZR enters market | + +### AI Optics Market Specifically + +| Year | AI Optics Market | Notes | +|---|---|---| +| 2023 | ~$3B | GPU interconnect demand begins | +| 2024 | ~$5B | NVIDIA GB200 drives 800G demand | +| 2025 | ~$7-8B | 800G mainstream for AI | +| 2026 (est.) | ~$10B+ | 1.6T for AI clusters | + +### Vendor Count Per Standard (approximate) + +| Standard/Type | Vendor Count (2025) | Notes | +|---|---|---| +| 1G SFP | 100+ | Fully commoditized | +| 10G SFP+ | 80+ | Commoditized | +| 25G SFP28 | 50+ | Maturing | +| 100G QSFP28 | 40+ | Maturing | +| 400G QSFP-DD | 25+ | Mainstream competition | +| 400G ZR/ZR+ | 10-15 | Specialized | +| 800G OSFP/QSFP-DD | 15-20 | Growing | +| 800G ZR/ZR+ | 5-8 | Emerging | +| 1.6T OSFP/OSFP-XD | 8-12 | Early stage | + +--- + +## 8. Database Schema Recommendations + +### Core Tables + +``` +TABLE: form_factors +- id, name, year_introduced, year_mainstream, year_decline, year_obsolete +- max_speed_gbps, connector_type, lane_count, width_mm, depth_mm +- power_class_w, backward_compatible_with, msa_spec_url +- status (emerging/active/declining/legacy/obsolete) + +TABLE: speed_tiers +- id, speed_gbps, year_standardized, year_mainstream, year_peak, year_decline +- primary_ieee_standard, modulation_type, lanes_config +- typical_launch_asp_usd, current_asp_usd + +TABLE: standards +- id, name, organization (IEEE/OIF/MSA), year_published, year_ratified +- speed_gbps, reach_km, key_phy_types, status + +TABLE: manufacturers +- id, name, hq_country, year_founded, specialties +- annual_revenue_usd, market_rank, key_products + +TABLE: market_data (time series) +- id, year, quarter, metric_name, metric_value, unit +- segment (datacom/telecom/enterprise), source + +TABLE: products +- id, manufacturer_id, form_factor_id, speed_tier_id +- model_name, wavelength_nm, reach_km, fiber_type +- modulation, fec_type, power_w, temperature_range +- year_launched, current_asp_usd, status + +TABLE: technology_transitions +- id, technology_name, category (modulation/integration/packaging) +- year_introduced, year_mainstream, year_peak +- market_share_pct, hype_cycle_phase + +TABLE: acquisitions +- id, acquirer_id, target_name, year, value_usd, strategic_rationale +``` + +--- + +## 9. Hype Cycle Analysis + +### Technology Hype Cycle Positions (2026) + +``` +PEAK OF INFLATED EXPECTATIONS: + - Co-Packaged Optics (CPO) + - 3.2T transceivers + - Optical compute interconnect + +SLOPE OF ENLIGHTENMENT: + - 1.6T pluggable transceivers + - Linear Drive Optics (LPO) + - 200G/lane PAM4 + - Near-Packaged Optics (NPO) + +PLATEAU OF PRODUCTIVITY: + - 800G pluggable transceivers + - 400ZR/ZR+ coherent pluggables + - Silicon photonics (in 400G/800G) + - PAM4 modulation + +ENTERING DECLINE: + - 400G pluggable (mainstream, starting decline) + - 100G QSFP28 (commoditized) + - NRZ modulation (for new designs) + +OBSOLESCENCE TRAJECTORY: + - 40G QSFP+ + - 10G XFP + - CFP/CFP2/CFP4 (except DCO) + - CWDM for high-speed (>100G) +``` + +### Form Factor Hype Cycles (Historical Overlay) + +``` + Innovation Peak Trough Slope Plateau + Trigger Hype Disillusion Enlighten Productivity +GBIC 1995 1998 2002 - 2000-2004 +SFP 2001 2003 - 2004 2005-forever +XFP 2002 2005 2008 - 2006-2012 +SFP+ 2006 2008 - 2009 2010-forever +QSFP+ 2012 2014 - 2015 2016-2022 +QSFP28 2014 2016 - 2017 2018-2024 +QSFP-DD 2019 2021 - 2022 2023-present +OSFP 2016 2020 - 2022 2023-present +OSFP-XD 2022 2025 - 2026(est) 2027(est) +``` + +### Speed Tier Lifecycle Model + +Each speed tier follows a predictable ~10-year lifecycle: + +``` +Years 0-2: INTRODUCTION - Standards ratified, samples shipping, $$$$ pricing +Years 2-4: GROWTH - Volume ramps, multiple vendors, pricing drops 40-60% +Years 4-6: MAINSTREAM - Peak shipments, broad adoption, pricing drops another 30-50% +Years 6-8: MATURITY - Pricing floor, commoditized, Chinese competition dominant +Years 8-10: DECLINE - Next-gen overtakes, volumes drop, maintenance-only +Years 10+: LEGACY - Minimal shipments, long-tail demand +``` + +| Speed | Introduction | Growth | Mainstream | Maturity | Decline | Legacy | +|---|---|---|---|---|---|---| +| 1G | 1998-2002 | 2002-2005 | 2005-2010 | 2010-2016 | 2016-2020 | 2020+ | +| 10G | 2002-2006 | 2006-2010 | 2010-2016 | 2016-2022 | 2022+ | - | +| 25G | 2014-2017 | 2017-2019 | 2019-2023 | 2023-2026 | 2026+ | - | +| 40G | 2010-2013 | 2013-2015 | 2015-2019 | 2019-2022 | 2022+ | - | +| 100G | 2014-2017 | 2017-2020 | 2020-2024 | 2024-2026 | 2026+ | - | +| 400G | 2020-2022 | 2022-2024 | 2024-2027 | 2027-2030 | 2030+ | - | +| 800G | 2023-2025 | 2025-2027 | 2027-2030 | 2030-2033 | 2033+ | - | +| 1.6T | 2025-2027 | 2027-2029 | 2029-2032 | 2032-2035 | 2035+ | - | + +--- + +## Sources & References + +### Market Research +- [Cignal AI - 800GbE Optics Shipments](https://cignal.ai/2025/05/800gbe-optics-shipments-to-grow-60-in-2025/) +- [Cignal AI - 20M 400G/800G Shipments 2024](https://cignal.ai/2025/01/over-20-million-400g-800g-datacom-optical-module-shipments-expected-for-2024/) +- [LightCounting - Silicon Photonics May 2025](https://www.lightcounting.com/newsletter/en/may-2025-silicon-photonics-linear-drive-pluggable-and-cpo-updated-november-2025-334) +- [LightCounting - AI Optics Jan 2025](https://www.lightcounting.com/newsletter/en/january-2025-optics-for-ai-clusters-319) +- [Mordor Intelligence - Optical Transceiver Market](https://www.mordorintelligence.com/industry-reports/optical-transceiver-market) +- [MarketsAndMarkets - Optical Transceiver Market 2030](https://www.marketsandmarkets.com/Market-Reports/optical-transceiver-market-161339599.html) +- [Fortune Business Insights - Optical Transceiver Market](https://www.fortunebusinessinsights.com/optical-transceiver-market-108985) + +### Standards & Specifications +- [IEEE 802.3 Working Group Archive](https://www.ieee802.org/3/archive.html) +- [IEEE 802.3 - Wikipedia](https://en.wikipedia.org/wiki/IEEE_802.3) +- [OIF 400ZR Implementation Agreement](https://www.oiforum.com/technical-work/hot-topics/400zr-2/) +- [OpenZR+ MSA](https://openzrplus.org/) +- [100G Lambda MSA](https://100glambda.com/) +- [CWDM4 MSA](https://cwdm4-msa.org/) +- [100G PSM4 MSA](http://www.psm4.org/) + +### Form Factors & Technology +- [Prooptix - History of Form Factors](https://www.prooptix.com/news/transceiver-form-factors/) +- [Meticulous Research - 25 Years of Optical Transceiver Evolution](https://www.meticulousresearch.com/blog/207/evolution-of-optical-transceiver-technologies-in-the-last-25-years) +- [Vitex - Transceiver Form Factors Guide](https://www.vitextech.com/blogs/blog/transceiver-form-factors) +- [FS.com - High-Speed Transceivers Guide](https://www.fs.com/blog/a-comprehensive-guide-to-highspeed-transceivers-400g-800g-and-the-leap-to-16t-13767.html) + +### Coherent Optics & WDM +- [WWT - 400G-ZR & ZR+ Guide](https://www.wwt.com/blog/400gzr-and-zr-the-latest-in-pluggable-coherent-dwdm) +- [Acacia/Cisco - 2024 Coherent Optics Review](https://acacia-inc.com/blog/a-look-back-at-2024-whats-ahead-for-coherent-optics-in-2025/) +- [Smartoptics - CWDM DWDM Explained](https://smartoptics.com/knowledgebank-post/cwdm-dwdm-explained/) +- [FS.com - DWDM/CWDM ITU Channels Guide](https://www.fs.com/blog/dwdmcwdm-wavelength-itu-channels-guide-3149.html) + +### Manufacturer & Industry Analysis +- [Iamfabian - 800G/1.6T Transceiver Battle](https://iamfabian.substack.com/p/pluggables-power-and-geopolitics) +- [Deep Fundamental - Optical Module Market Deep Dive](https://deepfundamental.substack.com/p/deep-dive-optical-module-market) +- [Chinese Suppliers Dominate 2024 Rankings](https://www.opticaltransceivermodules.com/news/chinese-optical-transceiver-suppliers-dominate-global-rankings-225829.html) +- [Coherent Corp - Wikipedia](https://en.wikipedia.org/wiki/Coherent_Corp.) + +### Next-Gen Technology +- [EDN - Co-Packaged Optics 2026](https://www.edn.com/where-co-packaged-optics-cpo-technology-stands-in-2026/) +- [IDTechEx - CPO Market Forecast](https://www.idtechex.com/en/research-report/co-packaged-optics-cpo/1138) +- [Eoptolink - Gen2 1.6T at OFC 2025](https://www.eoptolink.com/news/361-eoptolink-launches-its-gen2-1-6t-osfp-and-osfp-rhs-transceiver-family-at-ofc-2025) +- [Broadcom OFC 2025 Advances](https://investors.broadcom.com/news-releases/news-release-details/broadcom-advances-optical-connectivity-ai-infrastructure) +- [Jabil 1.6T Transceiver Launch](https://investors.jabil.com/news/news-details/2025/Jabil-Launches-1-6T-Pluggable-Transceiver-to-Support-Growing-Demand-for-Intra-Data-Center-and-AI-Connectivity/default.aspx) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..08f86bf --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,33 @@ +services: + postgres: + image: timescale/timescaledb:latest-pg17 + container_name: tip-postgres + environment: + POSTGRES_DB: transceiver_db + POSTGRES_USER: tip + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-tip_dev_2026} + ports: + - "5433:5432" + volumes: + - tip_pgdata:/var/lib/postgresql/data + - ./sql:/docker-entrypoint-initdb.d + healthcheck: + test: ["CMD-SHELL", "pg_isready -U tip -d transceiver_db"] + interval: 5s + timeout: 5s + retries: 5 + + qdrant: + image: qdrant/qdrant:latest + container_name: tip-qdrant + ports: + - "6333:6333" + - "6334:6334" + volumes: + - tip_qdrant:/qdrant/storage + environment: + QDRANT__SERVICE__GRPC_PORT: 6334 + +volumes: + tip_pgdata: + tip_qdrant: diff --git a/ecosystem.config.js b/ecosystem.config.js new file mode 100644 index 0000000..80c187d --- /dev/null +++ b/ecosystem.config.js @@ -0,0 +1,16 @@ +module.exports = { + apps: [ + { + name: "tip-api", + script: "packages/api/dist/index.js", + instances: 1, + autorestart: true, + watch: false, + max_memory_restart: "512M", + env: { + NODE_ENV: "production", + API_PORT: 3200, + }, + }, + ], +}; diff --git a/package-lock.json b/package-lock.json index 6bb7f9e..a60b11f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,20 +1,5218 @@ { - "name": "transceiver-db", - "version": "1.0.0", + "name": "transceiver-intelligence-platform", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "transceiver-db", - "version": "1.0.0", + "name": "transceiver-intelligence-platform", + "version": "0.1.0", "license": "MIT", + "workspaces": [ + "packages/*" + ], "devDependencies": { + "tsx": "^4.19", "typescript": "^5.9.3" + } + }, + "node_modules/@apify/consts": { + "version": "2.51.1", + "resolved": "https://registry.npmjs.org/@apify/consts/-/consts-2.51.1.tgz", + "integrity": "sha512-QV16f41BjmE7uYQgB+JeS5bhbEdFvP8eF1R5LiKlvGkERckSlMl1JIIaW1b/XwJdp3bEBKBGPtNlvYa06wyhwg==", + "license": "Apache-2.0" + }, + "node_modules/@apify/datastructures": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@apify/datastructures/-/datastructures-2.0.3.tgz", + "integrity": "sha512-E6yQyc/XZDqJopbaGmhzZXMJqwGf96ELtDANZa0t68jcOAJZS+pF7YUfQOLszXq6JQAdnRvTH2caotL6urX7HA==", + "license": "Apache-2.0" + }, + "node_modules/@apify/log": { + "version": "2.5.33", + "resolved": "https://registry.npmjs.org/@apify/log/-/log-2.5.33.tgz", + "integrity": "sha512-rD+RY/Lvgy2ZAQD6QHbzoGHKvqILSXHZggTv2PN80ZZl7JMVQ22pYpoysYITHl4eGuievCiwrhkvdbNqTHqoPQ==", + "license": "Apache-2.0", + "dependencies": { + "@apify/consts": "^2.51.1", + "ansi-colors": "^4.1.1" + } + }, + "node_modules/@apify/ps-tree": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@apify/ps-tree/-/ps-tree-1.2.0.tgz", + "integrity": "sha512-VHIswI7rD/R4bToeIDuJ9WJXt+qr5SdhfoZ9RzdjmCs9mgy7l0P4RugQEUCcU+WB4sfImbd4CKwzXcn0uYx1yw==", + "license": "MIT", + "dependencies": { + "event-stream": "3.3.4" + }, + "bin": { + "ps-tree": "bin/ps-tree.js" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@apify/pseudo_url": { + "version": "2.0.74", + "resolved": "https://registry.npmjs.org/@apify/pseudo_url/-/pseudo_url-2.0.74.tgz", + "integrity": "sha512-iMa7MzKn/5dWwSmOj3jZ+33NCRUdbyKsOTZytlowQgblV3yL8YFLziWcA1GlH6spIHG8073gIQMOecXvQYpvNA==", + "license": "Apache-2.0", + "dependencies": { + "@apify/log": "^2.5.33" + } + }, + "node_modules/@apify/timeout": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@apify/timeout/-/timeout-0.3.2.tgz", + "integrity": "sha512-JnOLIOpqfm366q7opKrA6HrL0iYRpYYDn8Mi77sMR2GZ1fPbwMWCVzN23LJWfJV7izetZbCMrqRUXsR1etZ7dA==", + "license": "Apache-2.0" + }, + "node_modules/@apify/utilities": { + "version": "2.25.5", + "resolved": "https://registry.npmjs.org/@apify/utilities/-/utilities-2.25.5.tgz", + "integrity": "sha512-I53XgSbNw2mYHPbPTIM7CjooHBHapWzvW6eKxpzt5IO9zB3OIzWOk2xRCodi1pAt3+A+BGiJJyddF/cQYGJenA==", + "license": "Apache-2.0", + "dependencies": { + "@apify/consts": "^2.51.1", + "@apify/log": "^2.5.33" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@crawlee/basic": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/@crawlee/basic/-/basic-3.16.0.tgz", + "integrity": "sha512-dcqeDkYk6NoXHSBEkALD4orb7k6yTDkwZp8RtcvlmMmVZKVQTVVHh78NFInzxRkjFVmStFWE2LRHBZpe518E0Q==", + "license": "Apache-2.0", + "dependencies": { + "@apify/log": "^2.4.0", + "@apify/timeout": "^0.3.0", + "@apify/utilities": "^2.7.10", + "@crawlee/core": "3.16.0", + "@crawlee/types": "3.16.0", + "@crawlee/utils": "3.16.0", + "csv-stringify": "^6.2.0", + "fs-extra": "^11.0.0", + "got-scraping": "^4.0.0", + "ow": "^0.28.1", + "tldts": "^7.0.0", + "tslib": "^2.4.0", + "type-fest": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@crawlee/browser": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/@crawlee/browser/-/browser-3.16.0.tgz", + "integrity": "sha512-7AJeJ5328qsgFhyITNt0V4YVtA5+t/yRtkiHIN5af4Ht/WlYaVTkY4Qs5a8c6x1NU9+bt14umEFcCAu2hGJMzw==", + "license": "Apache-2.0", + "dependencies": { + "@apify/timeout": "^0.3.0", + "@crawlee/basic": "3.16.0", + "@crawlee/browser-pool": "3.16.0", + "@crawlee/types": "3.16.0", + "@crawlee/utils": "3.16.0", + "ow": "^0.28.1", + "tslib": "^2.4.0", + "type-fest": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "playwright": "*", + "puppeteer": "*" + }, + "peerDependenciesMeta": { + "playwright": { + "optional": true + }, + "puppeteer": { + "optional": true + } + } + }, + "node_modules/@crawlee/browser-pool": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/@crawlee/browser-pool/-/browser-pool-3.16.0.tgz", + "integrity": "sha512-o9RK/TcDwxXF2wa5Ij6oG8JeS/aBTp/Xi4Rj8waQ/NKVfhF4DcOAlqiL/ed1YUgFUZx+P/VL/AIQKWbKAWnQlw==", + "license": "Apache-2.0", + "dependencies": { + "@apify/log": "^2.4.0", + "@apify/timeout": "^0.3.0", + "@crawlee/core": "3.16.0", + "@crawlee/types": "3.16.0", + "fingerprint-generator": "^2.1.68", + "fingerprint-injector": "^2.1.68", + "lodash.merge": "^4.6.2", + "nanoid": "^3.3.4", + "ow": "^0.28.1", + "p-limit": "^3.1.0", + "proxy-chain": "^2.0.1", + "quick-lru": "^5.1.1", + "tiny-typed-emitter": "^2.1.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "playwright": "*", + "puppeteer": "*" + }, + "peerDependenciesMeta": { + "playwright": { + "optional": true + }, + "puppeteer": { + "optional": true + } + } + }, + "node_modules/@crawlee/cheerio": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/@crawlee/cheerio/-/cheerio-3.16.0.tgz", + "integrity": "sha512-eyiWyHBuYZ0Ay5Q8wRD05RAAfgINxngUtlmUrV8r98Jpx9ibvm4UOS5yiqrZfGN2aoA31vasomCpgIcigacf8Q==", + "license": "Apache-2.0", + "dependencies": { + "@crawlee/http": "3.16.0", + "@crawlee/types": "3.16.0", + "@crawlee/utils": "3.16.0", + "cheerio": "1.0.0-rc.12", + "htmlparser2": "^9.0.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@crawlee/cheerio/node_modules/cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/@crawlee/cheerio/node_modules/cheerio/node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/@crawlee/cheerio/node_modules/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, + "node_modules/@crawlee/cli": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/@crawlee/cli/-/cli-3.16.0.tgz", + "integrity": "sha512-oZW2TEpcCYZmRvTtdeC57B7kgenvDbKf4GclDRZ/IH0aUnK7Zy0voTIEoqemyQdvbVN0NK43ylmZMz6KVdVygw==", + "license": "Apache-2.0", + "dependencies": { + "@crawlee/templates": "3.16.0", + "ansi-colors": "^4.1.3", + "fs-extra": "^11.0.0", + "inquirer": "^8.2.4", + "tslib": "^2.4.0", + "yargonaut": "^1.1.4", + "yargs": "^17.5.1" + }, + "bin": { + "crawlee": "index.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@crawlee/core": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/@crawlee/core/-/core-3.16.0.tgz", + "integrity": "sha512-Yn32E5IdmENLITg36XN1ty4OLPMcqzDjkEvSdZ0dRV5jcJR89sKi47FOs2eXpW+n7IGhbzPDkGKUirPPRrRkjg==", + "license": "Apache-2.0", + "dependencies": { + "@apify/consts": "^2.20.0", + "@apify/datastructures": "^2.0.0", + "@apify/log": "^2.4.0", + "@apify/pseudo_url": "^2.0.30", + "@apify/timeout": "^0.3.0", + "@apify/utilities": "^2.7.10", + "@crawlee/memory-storage": "3.16.0", + "@crawlee/types": "3.16.0", + "@crawlee/utils": "3.16.0", + "@sapphire/async-queue": "^1.5.1", + "@vladfrangu/async_event_emitter": "^2.2.2", + "csv-stringify": "^6.2.0", + "fs-extra": "^11.0.0", + "got-scraping": "^4.0.0", + "json5": "^2.2.3", + "minimatch": "^9.0.0", + "ow": "^0.28.1", + "stream-json": "^1.8.0", + "tldts": "^7.0.0", + "tough-cookie": "^6.0.0", + "tslib": "^2.4.0", + "type-fest": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@crawlee/http": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/@crawlee/http/-/http-3.16.0.tgz", + "integrity": "sha512-adp8fuQyW32kVKKJNPOA/HEF893ddPqldlIOcO+CdCa4EkeKTPOx74VGLVZyO4f0Zxs0QwvDL1W5O7ckD82MFQ==", + "license": "Apache-2.0", + "dependencies": { + "@apify/timeout": "^0.3.0", + "@apify/utilities": "^2.7.10", + "@crawlee/basic": "3.16.0", + "@crawlee/types": "3.16.0", + "@crawlee/utils": "3.16.0", + "@types/content-type": "^1.1.5", + "cheerio": "1.0.0-rc.12", + "content-type": "^1.0.4", + "got-scraping": "^4.0.0", + "iconv-lite": "^0.7.0", + "mime-types": "^2.1.35", + "ow": "^0.28.1", + "tslib": "^2.4.0", + "type-fest": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@crawlee/http/node_modules/cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/@crawlee/http/node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/@crawlee/http/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@crawlee/http/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@crawlee/jsdom": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/@crawlee/jsdom/-/jsdom-3.16.0.tgz", + "integrity": "sha512-dL+uOQrA7BGJN6PnqXe1Kcp76KyoLm5DSNkytZzeJm6ZphC/aOZUrC2a6SKU4XUnxVipnM6Nase/F+a1aNez1g==", + "license": "Apache-2.0", + "dependencies": { + "@apify/timeout": "^0.3.0", + "@apify/utilities": "^2.7.10", + "@crawlee/http": "3.16.0", + "@crawlee/types": "3.16.0", + "@crawlee/utils": "3.16.0", + "@types/jsdom": "^21.0.0", + "cheerio": "1.0.0-rc.12", + "jsdom": "^26.0.0", + "ow": "^0.28.2", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@crawlee/jsdom/node_modules/cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/@crawlee/jsdom/node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/@crawlee/linkedom": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/@crawlee/linkedom/-/linkedom-3.16.0.tgz", + "integrity": "sha512-AkpqiAqddk35gl2lNqDySuN5Raam1y3bQs49Y2NALc/TEnodXnnRO0rEEOh1P/wHNh4cm1jgY9rxmt/SHf3SLg==", + "license": "Apache-2.0", + "dependencies": { + "@apify/timeout": "^0.3.0", + "@apify/utilities": "^2.7.10", + "@crawlee/http": "3.16.0", + "@crawlee/types": "3.16.0", + "linkedom": "^0.18.0", + "ow": "^0.28.2", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@crawlee/memory-storage": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/@crawlee/memory-storage/-/memory-storage-3.16.0.tgz", + "integrity": "sha512-ol1PSWj5LL1ALjEZ+zJdLaZx4bGPIP6vXly4AmbtyFg2iq+m1BudtXL+dWFdv/qN8f+N8ljPF5VwKAVxg2uy3Q==", + "license": "Apache-2.0", + "dependencies": { + "@apify/log": "^2.4.0", + "@crawlee/types": "3.16.0", + "@sapphire/async-queue": "^1.5.0", + "@sapphire/shapeshift": "^3.0.0", + "content-type": "^1.0.4", + "fs-extra": "^11.0.0", + "json5": "^2.2.3", + "mime-types": "^2.1.35", + "proper-lockfile": "^4.1.2", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">= 16" + } + }, + "node_modules/@crawlee/memory-storage/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@crawlee/memory-storage/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@crawlee/playwright": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/@crawlee/playwright/-/playwright-3.16.0.tgz", + "integrity": "sha512-Oa7emJBmcqOcw/3iMc6KjfZUFAV2jmbvEv9jZQcMWPuVlmDVxV5Q67q0PF4/YDMesx0RBHLK0LRBcqO5jgtjFg==", + "license": "Apache-2.0", + "dependencies": { + "@apify/datastructures": "^2.0.0", + "@apify/log": "^2.4.0", + "@apify/timeout": "^0.3.1", + "@crawlee/browser": "3.16.0", + "@crawlee/browser-pool": "3.16.0", + "@crawlee/core": "3.16.0", + "@crawlee/types": "3.16.0", + "@crawlee/utils": "3.16.0", + "cheerio": "1.0.0-rc.12", + "jquery": "^3.6.0", + "lodash.isequal": "^4.5.0", + "ml-logistic-regression": "^2.0.0", + "ml-matrix": "^6.11.0", + "ow": "^0.28.1", + "string-comparison": "^1.3.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "idcac-playwright": "^0.2.0", + "playwright": "*" + }, + "peerDependenciesMeta": { + "idcac-playwright": { + "optional": true + }, + "playwright": { + "optional": true + } + } + }, + "node_modules/@crawlee/playwright/node_modules/cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/@crawlee/playwright/node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/@crawlee/puppeteer": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/@crawlee/puppeteer/-/puppeteer-3.16.0.tgz", + "integrity": "sha512-7qrh684m9bx1y7d+SRILlKelLk8FMML5lekMgiMzEQ7rjzrgXwWo3A9mkL9zQeC931pAMnWMZuGESloOYM2SxA==", + "license": "Apache-2.0", + "dependencies": { + "@apify/datastructures": "^2.0.0", + "@apify/log": "^2.4.0", + "@crawlee/browser": "3.16.0", + "@crawlee/browser-pool": "3.16.0", + "@crawlee/types": "3.16.0", + "@crawlee/utils": "3.16.0", + "cheerio": "1.0.0-rc.12", + "devtools-protocol": "*", + "idcac-playwright": "^0.2.0", + "jquery": "^3.6.0", + "ow": "^0.28.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "idcac-playwright": "^0.2.0", + "puppeteer": "*" + }, + "peerDependenciesMeta": { + "idcac-playwright": { + "optional": true + }, + "puppeteer": { + "optional": true + } + } + }, + "node_modules/@crawlee/puppeteer/node_modules/cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/@crawlee/puppeteer/node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/@crawlee/templates": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/@crawlee/templates/-/templates-3.16.0.tgz", + "integrity": "sha512-zDfRWDrqe75WEPtoUXGKA/iGmG+EHlepd0jc64AO1mUpZkOUVCNgSMxvMjxQV6zUMChsbPPvhOV6bHnY8/bEHA==", + "license": "Apache-2.0", + "dependencies": { + "ansi-colors": "^4.1.3", + "inquirer": "^9.0.0", + "tslib": "^2.4.0", + "yargonaut": "^1.1.4", + "yargs": "^17.5.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@crawlee/templates/node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@crawlee/templates/node_modules/inquirer": { + "version": "9.3.8", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.3.8.tgz", + "integrity": "sha512-pFGGdaHrmRKMh4WoDDSowddgjT1Vkl90atobmTeSmcPGdYiwikch/m/Ef5wRaiamHejtw0cUUMMerzDUXCci2w==", + "license": "MIT", + "dependencies": { + "@inquirer/external-editor": "^1.0.2", + "@inquirer/figures": "^1.0.3", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "1.0.0", + "ora": "^5.4.1", + "run-async": "^3.0.0", + "rxjs": "^7.8.1", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@crawlee/templates/node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@crawlee/templates/node_modules/run-async": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", + "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/@crawlee/types": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/@crawlee/types/-/types-3.16.0.tgz", + "integrity": "sha512-CcIM+JDVx4gzQzMPl+9RJiEeqdzTrx2RLPA7y4IMJSyfZm3J/VrEunielKA3NQrk095j9OuvS/rQL2y8mBV1qw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@crawlee/utils": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/@crawlee/utils/-/utils-3.16.0.tgz", + "integrity": "sha512-rfVx/3hsFZjiD4AwT8IoQsuNLiawrsdhc893Nha22mWQMxJ0Z/KUzh8FyJDnNOHuxWGIJP96I7nBikxYeSdw5A==", + "license": "Apache-2.0", + "dependencies": { + "@apify/log": "^2.4.0", + "@apify/ps-tree": "^1.2.0", + "@crawlee/types": "3.16.0", + "@types/sax": "^1.2.7", + "cheerio": "1.0.0-rc.12", + "file-type": "^20.0.0", + "got-scraping": "^4.0.3", + "ow": "^0.28.1", + "robots-parser": "^3.0.1", + "sax": "^1.4.1", + "tslib": "^2.4.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@crawlee/utils/node_modules/cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/@crawlee/utils/node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "license": "MIT" + }, + "node_modules/@sapphire/async-queue": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", + "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/shapeshift": { + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-3.9.7.tgz", + "integrity": "sha512-4It2mxPSr4OGn4HSQWGmhFMsNFGfFVhWeRPCRwbH972Ek2pzfGRZtb0pJ4Ze6oIzcyh2jw7nUDa6qGlWofgd9g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v16" + } + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@tip/api": { + "resolved": "packages/api", + "link": true + }, + "node_modules/@tip/core": { + "resolved": "packages/core", + "link": true + }, + "node_modules/@tip/scraper": { + "resolved": "packages/scraper", + "link": true + }, + "node_modules/@tokenizer/inflate": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", + "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "fflate": "^0.8.2", + "token-types": "^6.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/content-type": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@types/content-type/-/content-type-1.1.9.tgz", + "integrity": "sha512-Hq9IMnfekuOCsEmYl4QX2HBrT+XsfXiupfrLLY8Dcf3Puf4BkBOxSbWYTITSOQAhJoYPBez+b4MJRpIYL65z8A==", + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "license": "MIT" + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsdom": { + "version": "21.1.7", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", + "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/sax": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz", + "integrity": "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "license": "MIT" + }, + "node_modules/@types/xml2js": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.14.tgz", + "integrity": "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vladfrangu/async_event_emitter": { + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz", + "integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/adm-zip": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", + "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.11", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.11.tgz", + "integrity": "sha512-DAKrHphkJyiGuau/cFieRYhcTFeK/lBuD++C7cZ6KZHbMhBrisoi+EvhQ5RZrIfV5qwsW8kgQ07JIC+MDJRAhg==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/byte-counter": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/byte-counter/-/byte-counter-0.1.0.tgz", + "integrity": "sha512-jheRLVMeUKrDBjVw2O5+k4EvR4t9wtxHL+bo/LxfkxsVeuGMy3a5SEGgXdAFA4FSzTrU8rQXQIrsZ3oBq5a0pQ==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacheable-lookup": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", + "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/cacheable-request": { + "version": "13.0.18", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-13.0.18.tgz", + "integrity": "sha512-rFWadDRKJs3s2eYdXlGggnBZKG7MTblkFBB0YllFds+UYnfogDp2wcR6JN97FhRkHTvq59n2vhNoHNZn29dh/Q==", + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "^4.0.4", + "get-stream": "^9.0.1", + "http-cache-semantics": "^4.2.0", + "keyv": "^5.5.5", + "mimic-response": "^4.0.0", + "normalize-url": "^8.1.1", + "responselike": "^4.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001781", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001781.tgz", + "integrity": "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "license": "MIT" + }, + "node_modules/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "license": "ISC", + "engines": { + "node": ">= 10" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/crawlee": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/crawlee/-/crawlee-3.16.0.tgz", + "integrity": "sha512-j7wBS81zU+z7MNIKUqJuYRDbKJHwn5sWkki08glAXj6+Ka7HgU6IONHmrv9qtUmb/0p0m5tcMNqItMfnvh6bHA==", + "license": "Apache-2.0", + "dependencies": { + "@crawlee/basic": "3.16.0", + "@crawlee/browser": "3.16.0", + "@crawlee/browser-pool": "3.16.0", + "@crawlee/cheerio": "3.16.0", + "@crawlee/cli": "3.16.0", + "@crawlee/core": "3.16.0", + "@crawlee/http": "3.16.0", + "@crawlee/jsdom": "3.16.0", + "@crawlee/linkedom": "3.16.0", + "@crawlee/playwright": "3.16.0", + "@crawlee/puppeteer": "3.16.0", + "@crawlee/utils": "3.16.0", + "import-local": "^3.1.0", + "tslib": "^2.4.0" + }, + "bin": { + "crawlee": "cli.js" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "idcac-playwright": "*", + "playwright": "*", + "puppeteer": "*" + }, + "peerDependenciesMeta": { + "idcac-playwright": { + "optional": true + }, + "playwright": { + "optional": true + }, + "puppeteer": { + "optional": true + } + } + }, + "node_modules/cron-parser": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", + "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", + "license": "MIT", + "dependencies": { + "luxon": "^3.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/csv-stringify": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-6.7.0.tgz", + "integrity": "sha512-UdtziYp5HuTz7e5j8Nvq+a/3HQo+2/aJZ9xntNTpmRRIg/3YYqDVgiS9fvAhtNbnyfbv2ZBe0bqCHqzhE7FqWQ==", + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT" + }, + "node_modules/decompress-response": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-10.0.0.tgz", + "integrity": "sha512-oj7KWToJuuxlPr7VV0vabvxEIiqNMo+q0NueIiL3XhtwC6FVOX7Hr1c0C4eD0bmf7Zr+S/dSf2xvkH3Ad6sU3Q==", + "license": "MIT", + "dependencies": { + "mimic-response": "^4.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1604597", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1604597.tgz", + "integrity": "sha512-7DH4+FDIwg5AxeW+kvFb5qxJuDLSNK2S9FurqLpggMrUxS3tlvN/J2kP6uOghn584shRnvKheKSSvS4bgnzWYA==", + "license": "BSD-3-Clause" + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-prop": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", + "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.326", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.326.tgz", + "integrity": "sha512-uRBlUfKKdsXMkiiOurgaybNC10tjrD+skXLEg7NHbm6h0uAoqj3xMb9uue5BfcSCXJ4mcyJMOucI6q55D7p6KQ==", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/encoding-sniffer/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-stream": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", + "integrity": "sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==", + "license": "MIT", + "dependencies": { + "duplexer": "~0.1.1", + "from": "~0", + "map-stream": "~0.1.0", + "pause-stream": "0.0.11", + "split": "0.3", + "stream-combiner": "~0.0.4", + "through": "~2.3.1" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, + "node_modules/figlet": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.11.0.tgz", + "integrity": "sha512-EEx3OS/l2bFqcUNN2NM9FPJp8vAMrgbCxsbl2hbcJNNxOEwVe3mEzrhan7TbJQViZa8mMqhihlbCaqD+LyYKTQ==", + "license": "MIT", + "dependencies": { + "commander": "^14.0.0" + }, + "bin": { + "figlet": "bin/index.js" + }, + "engines": { + "node": ">= 17.0.0" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/file-type": { + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-20.5.0.tgz", + "integrity": "sha512-BfHZtG/l9iMm4Ecianu7P8HRD2tBHLtjXinm4X62XBOYzi7CYA7jyqfJzOvXHqzVrVPYqBo2/GvbARMaaJkKVg==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.2.6", + "strtok3": "^10.2.0", + "token-types": "^6.0.0", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fingerprint-generator": { + "version": "2.1.81", + "resolved": "https://registry.npmjs.org/fingerprint-generator/-/fingerprint-generator-2.1.81.tgz", + "integrity": "sha512-R8Cgnv9AhsTG8MN+DCuFolq2cJPdTNDKOM11EaRSCfRBnBGsPWTTm9e3INld1rzU+bMITvqAcghlCjXOVCrYUA==", + "license": "Apache-2.0", + "dependencies": { + "generative-bayesian-network": "^2.1.81", + "header-generator": "^2.1.81", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fingerprint-injector": { + "version": "2.1.81", + "resolved": "https://registry.npmjs.org/fingerprint-injector/-/fingerprint-injector-2.1.81.tgz", + "integrity": "sha512-/HlE+pDTety9ygiYHdlh+7lDhrm5sxOB7ThWdhDwDVqSr7zI4D/Ruqhg7iDmxMLVWTcUCXsiA9h9tgQgSiPolw==", + "license": "Apache-2.0", + "dependencies": { + "fingerprint-generator": "^2.1.81", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "playwright": "^1.22.2", + "puppeteer": ">= 9.x" + }, + "peerDependenciesMeta": { + "playwright": { + "optional": true + }, + "puppeteer": { + "optional": true + } + } + }, + "node_modules/form-data-encoder": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-4.1.0.tgz", + "integrity": "sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", + "license": "MIT" + }, + "node_modules/fs-extra": { + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generative-bayesian-network": { + "version": "2.1.81", + "resolved": "https://registry.npmjs.org/generative-bayesian-network/-/generative-bayesian-network-2.1.81.tgz", + "integrity": "sha512-LrYK+CY5n21p437oahz8jRqTgw0i+S08H+ypag1sgZilfCj33k8Tp8kcFtPiWKsEEJ6niN9gRFP12+r06xB4rQ==", + "license": "Apache-2.0", + "dependencies": { + "adm-zip": "^0.5.9", + "tslib": "^2.4.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "14.6.6", + "resolved": "https://registry.npmjs.org/got/-/got-14.6.6.tgz", + "integrity": "sha512-QLV1qeYSo5l13mQzWgP/y0LbMr5Plr5fJilgAIwgnwseproEbtNym8xpLsDzeZ6MWXgNE6kdWGBjdh3zT/Qerg==", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^7.0.1", + "byte-counter": "^0.1.0", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^13.0.12", + "decompress-response": "^10.0.0", + "form-data-encoder": "^4.0.2", + "http2-wrapper": "^2.2.1", + "keyv": "^5.5.3", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^4.0.1", + "responselike": "^4.0.2", + "type-fest": "^4.26.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/got-scraping": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/got-scraping/-/got-scraping-4.2.1.tgz", + "integrity": "sha512-rhOlO1L4H4Cm31smHJqPtAaXOUrhSKsiTrbZSHKFQW1E/mkTDopnHHpRnXJpqzE0faj+zPsVQnyifIqO+K+cLQ==", + "license": "Apache-2.0", + "dependencies": { + "got": "^14.2.1", + "header-generator": "^2.1.41", + "http2-wrapper": "^2.2.0", + "mimic-response": "^4.0.0", + "ow": "^1.1.1", + "quick-lru": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/got-scraping/node_modules/@sindresorhus/is": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", + "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/got-scraping/node_modules/callsites": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-4.2.0.tgz", + "integrity": "sha512-kfzR4zzQtAE9PC7CzZsjl3aBNbXWuXiSeOCdLcPpBfGW8YuCqQHcRPFDbr/BPVmd3EEPVpuFzLyuT/cUhPr4OQ==", + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/got-scraping/node_modules/dot-prop": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-7.2.0.tgz", + "integrity": "sha512-Ol/IPXUARn9CSbkrdV4VJo7uCy1I3VuSiWCaFSg+8BdUOzF9n3jefIpcgAydvUZbTdEBZs2vEiTiS9m61ssiDA==", + "license": "MIT", + "dependencies": { + "type-fest": "^2.11.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/got-scraping/node_modules/ow": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ow/-/ow-1.1.1.tgz", + "integrity": "sha512-sJBRCbS5vh1Jp9EOgwp1Ws3c16lJrUkJYlvWTYC03oyiYVwS/ns7lKRWow4w4XjDyTrA2pplQv4B2naWSR6yDA==", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^5.3.0", + "callsites": "^4.0.0", + "dot-prop": "^7.2.0", + "lodash.isequal": "^4.5.0", + "vali-date": "^1.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/got-scraping/node_modules/quick-lru": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-7.3.0.tgz", + "integrity": "sha512-k9lSsjl36EJdK7I06v7APZCbyGT2vMTsYSRX1Q2nbYmnkBqgUhRkAuzH08Ciotteu/PLJmIF2+tti7o3C/ts2g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/got-scraping/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-ansi/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/header-generator": { + "version": "2.1.81", + "resolved": "https://registry.npmjs.org/header-generator/-/header-generator-2.1.81.tgz", + "integrity": "sha512-6+27UuqCHFx4xrTWIgcSF/x2WJ+PuVLxziXfPaVLRXi1lXIbTkXO+ffHJefVrdRT5/XEeWfJHrSIE2m1hAdWxw==", + "license": "Apache-2.0", + "dependencies": { + "browserslist": "^4.21.1", + "generative-bayesian-network": "^2.1.81", + "ow": "^0.28.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/helmet": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", + "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/html-escaper": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", + "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==", + "license": "MIT" + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http2-wrapper": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", + "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.2.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/idcac-playwright": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/idcac-playwright/-/idcac-playwright-0.2.0.tgz", + "integrity": "sha512-qJH7vQgq3TKnhea/3Z3jlEJL7NC9vK9BkLClAzQHVRepBtq1fWfSI4fSuMKcPq7nDUTTlIEIS+vU+GRwwR1BXw==", + "license": "GPL-3.0-only" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/inquirer": { + "version": "8.2.7", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.7.tgz", + "integrity": "sha512-UjOaSel/iddGZJ5xP/Eixh6dY1XghiBw4XK13rCCIJcJfyhhoul/7KhLLUGtebEj6GDYM6Vnx/mVsjx2L/mFIA==", + "license": "MIT", + "dependencies": { + "@inquirer/external-editor": "^1.0.0", + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^6.0.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-any-array": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-any-array/-/is-any-array-2.0.1.tgz", + "integrity": "sha512-UtilS7hLRu++wb/WBAw9bNuP1Eg04Ivn1vERJck8zJthEvXCBEBpGR/33u/xLKWEQf95803oalHrVDptcAvFdQ==", + "license": "MIT" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "license": "MIT" + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jquery": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/jsdom/node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "license": "MIT" + }, + "node_modules/jsdom/node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/linkedom": { + "version": "0.18.12", + "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.18.12.tgz", + "integrity": "sha512-jalJsOwIKuQJSeTvsgzPe9iJzyfVaEJiEXl+25EkKevsULHvMJzpNqwvj1jOESWdmgKDiXObyjOYwlUqG7wo1Q==", + "license": "ISC", + "dependencies": { + "css-select": "^5.1.0", + "cssom": "^0.5.0", + "html-escaper": "^3.0.3", + "htmlparser2": "^10.0.0", + "uhyphen": "^0.2.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "canvas": ">= 2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lowercase-keys": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/map-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", + "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", + "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ml-array-max": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/ml-array-max/-/ml-array-max-1.2.4.tgz", + "integrity": "sha512-BlEeg80jI0tW6WaPyGxf5Sa4sqvcyY6lbSn5Vcv44lp1I2GR6AWojfUvLnGTNsIXrZ8uqWmo8VcG1WpkI2ONMQ==", + "license": "MIT", + "dependencies": { + "is-any-array": "^2.0.0" + } + }, + "node_modules/ml-array-min": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/ml-array-min/-/ml-array-min-1.2.3.tgz", + "integrity": "sha512-VcZ5f3VZ1iihtrGvgfh/q0XlMobG6GQ8FsNyQXD3T+IlstDv85g8kfV0xUG1QPRO/t21aukaJowDzMTc7j5V6Q==", + "license": "MIT", + "dependencies": { + "is-any-array": "^2.0.0" + } + }, + "node_modules/ml-array-rescale": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/ml-array-rescale/-/ml-array-rescale-1.3.7.tgz", + "integrity": "sha512-48NGChTouvEo9KBctDfHC3udWnQKNKEWN0ziELvY3KG25GR5cA8K8wNVzracsqSW1QEkAXjTNx+ycgAv06/1mQ==", + "license": "MIT", + "dependencies": { + "is-any-array": "^2.0.0", + "ml-array-max": "^1.2.4", + "ml-array-min": "^1.2.3" + } + }, + "node_modules/ml-logistic-regression": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ml-logistic-regression/-/ml-logistic-regression-2.0.0.tgz", + "integrity": "sha512-xHhB91ut8GRRbJyB1ZQfKsl1MHmE1PqMeRjxhks96M5BGvCbC9eEojf4KgRMKM2LxFblhVUcVzweAoPB48Nt0A==", + "license": "MIT", + "dependencies": { + "ml-matrix": "^6.5.0" + } + }, + "node_modules/ml-matrix": { + "version": "6.12.1", + "resolved": "https://registry.npmjs.org/ml-matrix/-/ml-matrix-6.12.1.tgz", + "integrity": "sha512-TJ+8eOFdp+INvzR4zAuwBQJznDUfktMtOB6g/hUcGh3rcyjxbz4Te57Pgri8Q9bhSQ7Zys4IYOGhFdnlgeB6Lw==", + "license": "MIT", + "dependencies": { + "is-any-array": "^2.0.1", + "ml-array-rescale": "^1.3.7" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "license": "ISC" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "license": "MIT" + }, + "node_modules/normalize-url": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.1.tgz", + "integrity": "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ow": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/ow/-/ow-0.28.2.tgz", + "integrity": "sha512-dD4UpyBh/9m4X2NVjA+73/ZPBRF+uF4zIMFvvQsabMiEK8x41L3rQ8EENOi35kyyoaJwNxEeJcP6Fj1H4U409Q==", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.2.0", + "callsites": "^3.1.0", + "dot-prop": "^6.0.1", + "lodash.isequal": "^4.5.0", + "vali-date": "^1.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ow/node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/p-cancelable": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-4.0.1.tgz", + "integrity": "sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==", + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-require": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parent-require/-/parent-require-1.0.0.tgz", + "integrity": "sha512-2MXDNZC4aXdkkap+rBBMv0lUsfJqvX5/2FiYYnfCnorZt3Pk06/IOR5KeaoghgS2w07MLWgjbsnyaq6PdHn2LQ==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz", + "integrity": "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", + "license": [ + "MIT", + "Apache2" + ], + "dependencies": { + "through": "~2.3" + } + }, + "node_modules/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-boss": { + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/pg-boss/-/pg-boss-10.4.2.tgz", + "integrity": "sha512-AttEWOtSzn53av8OnCMWEanwRBvjkZCE1y5nLrZnwvkkMnlZ5XpWDpZ7sKI/BYjvi2OVieMX37arD2ACgJ750w==", + "license": "MIT", + "dependencies": { + "cron-parser": "^4.9.0", + "pg": "^8.16.3", + "serialize-error": "^8.1.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", + "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-chain": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/proxy-chain/-/proxy-chain-2.7.1.tgz", + "integrity": "sha512-LtXu0miohJYrHWJxv8wA6EoGreRcX1hxKb7qlE1pMFH+BXE7bqMvpyhzR/JvR6M5SzYKzyHFpvfmYJrZeMtwAg==", + "license": "Apache-2.0", + "dependencies": { + "socks": "^2.8.3", + "socks-proxy-agent": "^8.0.3", + "tslib": "^2.3.1" }, "engines": { "node": ">=14" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "license": "MIT" + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/responselike": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-4.0.2.tgz", + "integrity": "sha512-cGk8IbWEAnaCpdAt1BHzJ3Ahz5ewDJa0KseTsE3qIRMJ3C698W8psM7byCeWVpd/Ha7FUYzuRVzXoKoM6nRUbA==", + "license": "MIT", + "dependencies": { + "lowercase-keys": "^3.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/robots-parser": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/robots-parser/-/robots-parser-3.0.1.tgz", + "integrity": "sha512-s+pyvQeIKIZ0dx5iJiQk1tPLJAWln39+MI5jtM8wnyws+G5azk+dMnMX0qfbqNetKKNgcWWOdi0sfm+FbQbgdQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "license": "MIT" + }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serialize-error": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-8.1.0.tgz", + "integrity": "sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serialize-error/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/split": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", + "integrity": "sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==", + "license": "MIT", + "dependencies": { + "through": "2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stream-chain": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz", + "integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==", + "license": "BSD-3-Clause" + }, + "node_modules/stream-combiner": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", + "integrity": "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==", + "license": "MIT", + "dependencies": { + "duplexer": "~0.1.1" + } + }, + "node_modules/stream-json": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz", + "integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==", + "license": "BSD-3-Clause", + "dependencies": { + "stream-chain": "^2.2.5" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-comparison": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string-comparison/-/string-comparison-1.3.0.tgz", + "integrity": "sha512-46aD+slEwybxAMPRII83ATbgMgTiz5P8mVd7Z6VJsCzSHFjdt1hkAVLeFxPIyEb11tc6ihpJTlIqoO0MCF6NPw==", + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strtok3": { + "version": "10.3.5", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", + "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "license": "MIT" + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "license": "MIT" + }, + "node_modules/tiny-typed-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz", + "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==", + "license": "MIT" + }, + "node_modules/tldts": { + "version": "7.0.27", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.27.tgz", + "integrity": "sha512-I4FZcVFcqCRuT0ph6dCDpPuO4Xgzvh+spkcTr1gK7peIvxWauoloVO0vuy1FQnijT63ss6AsHB6+OIM4aXHbPg==", + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.27" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.27", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.27.tgz", + "integrity": "sha512-YQ7uPjgWUibIK6DW5lrKujGwUKhLevU4hcGbP5O6TcIUb+oTjJYJVWPS4nZsIHrEEEG6myk/oqAJUEQmpZrHsg==", + "license": "MIT" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -28,6 +5226,460 @@ "engines": { "node": ">=14.17" } + }, + "node_modules/uhyphen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz", + "integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==", + "license": "ISC" + }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.6.tgz", + "integrity": "sha512-Xi4agocCbRzt0yYMZGMA6ApD7gvtUFaxm4ZmeacWI4cZxaF6C+8I8QfofC20NAePiB/IcvZmzkJ7XPa471AEtA==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vali-date": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/vali-date/-/vali-date-1.0.0.tgz", + "integrity": "sha512-sgECfZthyaCKW10N0fm27cg8HYTFK5qMWgypqkXMQ4Wbl/zZKx7xZICgcoxIIE+WFAP/MBL2EFwC/YvLxw3Zeg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "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/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargonaut": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/yargonaut/-/yargonaut-1.1.4.tgz", + "integrity": "sha512-rHgFmbgXAAzl+1nngqOcwEljqHGG9uUZoPjsdZEs1w5JW9RXYzrSvH/u70C1JE5qFi0qjsdhnUX/dJRpWqitSA==", + "license": "Apache-2.0", + "dependencies": { + "chalk": "^1.1.1", + "figlet": "^1.1.1", + "parent-require": "^1.0.0" + } + }, + "node_modules/yargonaut/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yargonaut/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yargonaut/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yargonaut/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yargonaut/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "packages/api": { + "name": "@tip/api", + "version": "0.1.0", + "dependencies": { + "cors": "^2.8.5", + "dotenv": "^16.4.7", + "express": "^5.1.0", + "express-rate-limit": "^7.5.0", + "helmet": "^8.0.0", + "pg": "^8.13.1", + "zod": "^3.24.2" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^5.0.0", + "@types/pg": "^8.11.11", + "tsx": "^4.19.0", + "typescript": "^5.9.3" + } + }, + "packages/core": { + "name": "@tip/core", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "typescript": "^5.9.3" + }, + "engines": { + "node": ">=14" + } + }, + "packages/scraper": { + "name": "@tip/scraper", + "version": "0.1.0", + "dependencies": { + "cheerio": "^1.0.0", + "crawlee": "^3.12.0", + "dotenv": "^16.4.7", + "pg": "^8.13.1", + "pg-boss": "^10.1.5", + "playwright": "^1.50.0", + "xml2js": "^0.6.2" + }, + "devDependencies": { + "@types/pg": "^8.11.11", + "@types/xml2js": "^0.4.14", + "tsx": "^4.19.0", + "typescript": "^5.9.3" + } } } } diff --git a/package.json b/package.json index 87e7758..cb33398 100644 --- a/package.json +++ b/package.json @@ -1,45 +1,28 @@ { - "name": "transceiver-db", - "version": "1.0.0", - "description": "Open-source optical transceiver database. 89 products, 39 IEEE/MSA standards, 16 form factors, 9 speed tiers. SFP to 800G OSFP.", - "main": "dist/index.js", - "types": "dist/index.d.ts", + "name": "transceiver-intelligence-platform", + "version": "0.1.0", + "private": true, + "description": "Transceiver Intelligence Platform — the world's most comprehensive optical transceiver & network switch database", + "workspaces": [ + "packages/*" + ], "scripts": { - "build": "tsc", - "prepublishOnly": "npm run build" + "build": "npm run build --workspaces", + "build:core": "npm run build -w packages/core", + "build:api": "npm run build -w packages/api", + "dev": "npm run dev -w packages/api", + "migrate": "tsx scripts/migrate.ts", + "seed": "tsx scripts/seed-from-npm.ts", + "db:reset": "npm run migrate && npm run seed" }, + "author": "Rene Fichtmueller", "license": "MIT", - "keywords": [ - "transceiver", - "optics", - "sfp", - "qsfp", - "networking", - "fiber", - "ieee", - "telecom", - "osfp", - "qsfp-dd", - "optical", - "datacenter", - "100g", - "400g", - "800g" - ], - "files": [ - "dist", - "LICENSE", - "README.md" - ], "repository": { "type": "git", "url": "https://github.com/renefichtmueller/transceiver-db" }, - "author": "Rene Fichtmueller", - "engines": { - "node": ">=14" - }, "devDependencies": { + "tsx": "^4.19", "typescript": "^5.9.3" } } diff --git a/packages/api/package.json b/packages/api/package.json new file mode 100644 index 0000000..46bd5af --- /dev/null +++ b/packages/api/package.json @@ -0,0 +1,28 @@ +{ + "name": "@tip/api", + "version": "0.1.0", + "private": true, + "description": "TIP REST API server", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "dev": "tsx watch src/index.ts", + "start": "node dist/index.js" + }, + "dependencies": { + "express": "^5.1.0", + "pg": "^8.13.1", + "cors": "^2.8.5", + "dotenv": "^16.4.7", + "helmet": "^8.0.0", + "express-rate-limit": "^7.5.0", + "zod": "^3.24.2" + }, + "devDependencies": { + "@types/express": "^5.0.0", + "@types/pg": "^8.11.11", + "@types/cors": "^2.8.17", + "typescript": "^5.9.3", + "tsx": "^4.19.0" + } +} diff --git a/packages/api/src/config.ts b/packages/api/src/config.ts new file mode 100644 index 0000000..a8f3a3e --- /dev/null +++ b/packages/api/src/config.ts @@ -0,0 +1,20 @@ +import { config } from "dotenv"; +import { join } from "path"; + +config({ path: join(__dirname, "..", "..", "..", ".env") }); + +export const cfg = { + port: parseInt(process.env.API_PORT || "3200"), + nodeEnv: process.env.NODE_ENV || "development", + db: { + host: process.env.POSTGRES_HOST || "localhost", + port: parseInt(process.env.POSTGRES_PORT || "5432"), + database: process.env.POSTGRES_DB || "transceiver_db", + user: process.env.POSTGRES_USER || "tip", + password: process.env.POSTGRES_PASSWORD || "tip_dev_2026", + }, + qdrant: { + host: process.env.QDRANT_HOST || "localhost", + port: parseInt(process.env.QDRANT_PORT || "6333"), + }, +} as const; diff --git a/packages/api/src/db/client.ts b/packages/api/src/db/client.ts new file mode 100644 index 0000000..8f30a2f --- /dev/null +++ b/packages/api/src/db/client.ts @@ -0,0 +1,17 @@ +import { Pool } from "pg"; +import { cfg } from "../config"; + +export const pool = new Pool({ + host: cfg.db.host, + port: cfg.db.port, + database: cfg.db.database, + user: cfg.db.user, + password: cfg.db.password, + max: 20, + idleTimeoutMillis: 30000, + connectionTimeoutMillis: 5000, +}); + +pool.on("error", (err) => { + console.error("Unexpected PostgreSQL error:", err); +}); diff --git a/packages/api/src/db/queries.ts b/packages/api/src/db/queries.ts new file mode 100644 index 0000000..819038f --- /dev/null +++ b/packages/api/src/db/queries.ts @@ -0,0 +1,211 @@ +import { pool } from "./client"; + +export interface SearchParams { + q?: string; + form_factor?: string; + speed?: string; + speed_gbps?: number; + category?: string; + fiber_type?: string; + reach_min?: number; + reach_max?: number; + wdm_type?: string; + coherent?: boolean; + market_status?: string; + limit?: number; + offset?: number; +} + +export async function searchTransceivers(params: SearchParams) { + const conditions: string[] = []; + const values: any[] = []; + let idx = 1; + + if (params.q) { + conditions.push(`search_vector @@ plainto_tsquery('english', $${idx})`); + values.push(params.q); + idx++; + } + if (params.form_factor) { + conditions.push(`form_factor = $${idx}`); + values.push(params.form_factor); + idx++; + } + if (params.speed) { + conditions.push(`speed = $${idx}`); + values.push(params.speed); + idx++; + } + if (params.speed_gbps) { + conditions.push(`speed_gbps = $${idx}`); + values.push(params.speed_gbps); + idx++; + } + if (params.category) { + conditions.push(`category = $${idx}`); + values.push(params.category); + idx++; + } + if (params.fiber_type) { + conditions.push(`fiber_type = $${idx}`); + values.push(params.fiber_type); + idx++; + } + if (params.reach_min) { + conditions.push(`reach_meters >= $${idx}`); + values.push(params.reach_min); + idx++; + } + if (params.reach_max) { + conditions.push(`reach_meters <= $${idx}`); + values.push(params.reach_max); + idx++; + } + if (params.wdm_type) { + conditions.push(`wdm_type = $${idx}`); + values.push(params.wdm_type); + idx++; + } + if (params.coherent !== undefined) { + conditions.push(`coherent = $${idx}`); + values.push(params.coherent); + idx++; + } + if (params.market_status) { + conditions.push(`market_status = $${idx}`); + values.push(params.market_status); + idx++; + } + + const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""; + const limit = params.limit || 50; + const offset = params.offset || 0; + + // Add relevance ranking when full-text search is used + const orderBy = params.q + ? `ORDER BY ts_rank(search_vector, plainto_tsquery('english', $1)) DESC` + : `ORDER BY speed_gbps DESC, reach_meters ASC`; + + const query = ` + SELECT t.*, v.name as vendor_name + FROM transceivers t + LEFT JOIN vendors v ON t.vendor_id = v.id + ${where} + ${orderBy} + LIMIT ${limit} OFFSET ${offset} + `; + + const countQuery = `SELECT COUNT(*) FROM transceivers ${where}`; + + const [dataResult, countResult] = await Promise.all([ + pool.query(query, values), + pool.query(countQuery, values), + ]); + + return { + data: dataResult.rows, + total: parseInt(countResult.rows[0].count), + limit, + offset, + }; +} + +export async function getTransceiverById(id: string) { + const result = await pool.query( + `SELECT t.*, v.name as vendor_name, s.name as standard_full_name + FROM transceivers t + LEFT JOIN vendors v ON t.vendor_id = v.id + LEFT JOIN standards s ON t.standard_id = s.id + WHERE t.id = $1 OR t.slug = $1`, + [id] + ); + return result.rows[0] || null; +} + +export async function searchSwitches(params: SearchParams) { + const conditions: string[] = []; + const values: any[] = []; + let idx = 1; + + if (params.q) { + conditions.push(`sw.search_vector @@ plainto_tsquery('english', $${idx})`); + values.push(params.q); + idx++; + } + if (params.category) { + conditions.push(`sw.category = $${idx}`); + values.push(params.category); + idx++; + } + + const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""; + const limit = params.limit || 50; + const offset = params.offset || 0; + + const query = ` + SELECT sw.*, v.name as vendor_name + FROM switches sw + LEFT JOIN vendors v ON sw.vendor_id = v.id + ${where} + ORDER BY sw.max_speed_gbps DESC NULLS LAST + LIMIT ${limit} OFFSET ${offset} + `; + + const result = await pool.query(query, values); + return { data: result.rows, limit, offset }; +} + +export async function getSwitchById(id: string) { + const result = await pool.query( + `SELECT sw.*, v.name as vendor_name + FROM switches sw + LEFT JOIN vendors v ON sw.vendor_id = v.id + WHERE sw.id = $1`, + [id] + ); + return result.rows[0] || null; +} + +export async function getCompatibleTransceivers(switchId: string) { + const result = await pool.query( + `SELECT t.*, c.status, c.verified_by, c.notes as compat_notes + FROM compatibility c + JOIN transceivers t ON c.transceiver_id = t.id + WHERE c.switch_id = $1 AND c.status = 'compatible' + ORDER BY t.speed_gbps DESC`, + [switchId] + ); + return result.rows; +} + +export async function listVendors(type?: string) { + const query = type + ? `SELECT * FROM vendors WHERE type = $1 ORDER BY name` + : `SELECT * FROM vendors ORDER BY name`; + const result = await pool.query(query, type ? [type] : []); + return result.rows; +} + +export async function listStandards(speed?: string) { + const query = speed + ? `SELECT * FROM standards WHERE speed = $1 ORDER BY year_ratified DESC` + : `SELECT * FROM standards ORDER BY year_ratified DESC`; + const result = await pool.query(query, speed ? [speed] : []); + return result.rows; +} + +export async function getDbStats() { + const result = await pool.query(` + SELECT + (SELECT COUNT(*) FROM vendors) as vendor_count, + (SELECT COUNT(*) FROM standards) as standard_count, + (SELECT COUNT(*) FROM transceivers) as transceiver_count, + (SELECT COUNT(*) FROM switches) as switch_count, + (SELECT COUNT(*) FROM compatibility) as compatibility_count, + (SELECT COUNT(*) FROM breakouts) as breakout_count, + (SELECT COUNT(*) FROM knowledge_base) as kb_count, + (SELECT COUNT(*) FROM documents) as document_count, + (SELECT COUNT(*) FROM news_articles) as news_count + `); + return result.rows[0]; +} diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts new file mode 100644 index 0000000..a471dbf --- /dev/null +++ b/packages/api/src/index.ts @@ -0,0 +1,57 @@ +import express from "express"; +import cors from "cors"; +import helmet from "helmet"; +import rateLimit from "express-rate-limit"; +import { cfg } from "./config"; +import { transceiverRouter } from "./routes/transceivers"; +import { switchRouter } from "./routes/switches"; +import { vendorRouter } from "./routes/vendors"; +import { standardRouter } from "./routes/standards"; +import { healthRouter } from "./routes/health"; + +const app = express(); + +// Middleware +app.use(helmet()); +app.use(cors()); +app.use(express.json()); +app.use( + rateLimit({ + windowMs: 60 * 1000, + max: 200, + standardHeaders: true, + legacyHeaders: false, + }) +); + +// Routes +app.use("/api/transceivers", transceiverRouter); +app.use("/api/switches", switchRouter); +app.use("/api/vendors", vendorRouter); +app.use("/api/standards", standardRouter); +app.use("/api/health", healthRouter); + +// Root +app.get("/", (_req, res) => { + res.json({ + name: "Transceiver Intelligence Platform", + version: "0.1.0", + endpoints: [ + "GET /api/transceivers?q=&form_factor=&speed=&category=&fiber_type=&wdm_type=&coherent=", + "GET /api/transceivers/:id", + "GET /api/switches?q=&category=", + "GET /api/switches/:id", + "GET /api/switches/:id/compatibility", + "GET /api/vendors?type=", + "GET /api/standards?speed=", + "GET /api/health", + ], + }); +}); + +// Start +app.listen(cfg.port, () => { + console.log(`\n TIP API running on http://localhost:${cfg.port}`); + console.log(` Environment: ${cfg.nodeEnv}`); + console.log(` Database: ${cfg.db.host}:${cfg.db.port}/${cfg.db.database}\n`); +}); diff --git a/packages/api/src/routes/health.ts b/packages/api/src/routes/health.ts new file mode 100644 index 0000000..6505b92 --- /dev/null +++ b/packages/api/src/routes/health.ts @@ -0,0 +1,32 @@ +import { Router, Request, Response } from "express"; +import { getDbStats } from "../db/queries"; +import { pool } from "../db/client"; + +export const healthRouter = Router(); + +// GET /api/health — Health check with DB stats +healthRouter.get("/", async (_req: Request, res: Response) => { + try { + const start = Date.now(); + const stats = await getDbStats(); + const latencyMs = Date.now() - start; + + res.json({ + success: true, + status: "healthy", + version: "0.1.0", + uptime: process.uptime(), + database: { + connected: true, + latency_ms: latencyMs, + stats, + }, + }); + } catch (err) { + res.status(503).json({ + success: false, + status: "unhealthy", + database: { connected: false, error: String(err) }, + }); + } +}); diff --git a/packages/api/src/routes/standards.ts b/packages/api/src/routes/standards.ts new file mode 100644 index 0000000..48a2c9d --- /dev/null +++ b/packages/api/src/routes/standards.ts @@ -0,0 +1,15 @@ +import { Router, Request, Response } from "express"; +import { listStandards } from "../db/queries"; + +export const standardRouter = Router(); + +// GET /api/standards — List all standards +standardRouter.get("/", async (req: Request, res: Response) => { + try { + const standards = await listStandards(req.query.speed ? String(req.query.speed) : undefined); + res.json({ success: true, data: standards, total: standards.length }); + } catch (err) { + console.error("List standards error:", err); + res.status(500).json({ success: false, error: "Internal server error" }); + } +}); diff --git a/packages/api/src/routes/switches.ts b/packages/api/src/routes/switches.ts new file mode 100644 index 0000000..a5e3076 --- /dev/null +++ b/packages/api/src/routes/switches.ts @@ -0,0 +1,46 @@ +import { Router, Request, Response } from "express"; +import { searchSwitches, getSwitchById, getCompatibleTransceivers } from "../db/queries"; + +export const switchRouter = Router(); + +// GET /api/switches — Search/list switches +switchRouter.get("/", async (req: Request, res: Response) => { + try { + const result = await searchSwitches({ + q: String(req.query.q || ""), + category: req.query.category ? String(req.query.category) : undefined, + limit: req.query.limit ? parseInt(String(req.query.limit)) : 50, + offset: req.query.offset ? parseInt(String(req.query.offset)) : 0, + }); + res.json({ success: true, ...result }); + } catch (err) { + console.error("Search switches error:", err); + res.status(500).json({ success: false, error: "Internal server error" }); + } +}); + +// GET /api/switches/:id — Get single switch +switchRouter.get("/:id", async (req: Request, res: Response) => { + try { + const sw = await getSwitchById(String(req.params.id)); + if (!sw) { + res.status(404).json({ success: false, error: "Switch not found" }); + return; + } + res.json({ success: true, data: sw }); + } catch (err) { + console.error("Get switch error:", err); + res.status(500).json({ success: false, error: "Internal server error" }); + } +}); + +// GET /api/switches/:id/compatibility — Compatible transceivers for a switch +switchRouter.get("/:id/compatibility", async (req: Request, res: Response) => { + try { + const transceivers = await getCompatibleTransceivers(String(req.params.id)); + res.json({ success: true, data: transceivers, total: transceivers.length }); + } catch (err) { + console.error("Get compatibility error:", err); + res.status(500).json({ success: false, error: "Internal server error" }); + } +}); diff --git a/packages/api/src/routes/transceivers.ts b/packages/api/src/routes/transceivers.ts new file mode 100644 index 0000000..0313786 --- /dev/null +++ b/packages/api/src/routes/transceivers.ts @@ -0,0 +1,45 @@ +import { Router, Request, Response } from "express"; +import { searchTransceivers, getTransceiverById } from "../db/queries"; + +export const transceiverRouter = Router(); + +// GET /api/transceivers — Search/list transceivers +transceiverRouter.get("/", async (req: Request, res: Response) => { + try { + const q = (p: string) => req.query[p] ? String(req.query[p]) : undefined; + const result = await searchTransceivers({ + q: q("q"), + form_factor: q("form_factor"), + speed: q("speed"), + speed_gbps: q("speed_gbps") ? parseFloat(q("speed_gbps")!) : undefined, + category: q("category"), + fiber_type: q("fiber_type"), + reach_min: q("reach_min") ? parseInt(q("reach_min")!) : undefined, + reach_max: q("reach_max") ? parseInt(q("reach_max")!) : undefined, + wdm_type: q("wdm_type"), + coherent: q("coherent") === "true" ? true : q("coherent") === "false" ? false : undefined, + market_status: q("market_status"), + limit: q("limit") ? parseInt(q("limit")!) : 50, + offset: q("offset") ? parseInt(q("offset")!) : 0, + }); + res.json({ success: true, ...result }); + } catch (err) { + console.error("Search transceivers error:", err); + res.status(500).json({ success: false, error: "Internal server error" }); + } +}); + +// GET /api/transceivers/:id — Get single transceiver +transceiverRouter.get("/:id", async (req: Request, res: Response) => { + try { + const transceiver = await getTransceiverById(String(req.params.id)); + if (!transceiver) { + res.status(404).json({ success: false, error: "Transceiver not found" }); + return; + } + res.json({ success: true, data: transceiver }); + } catch (err) { + console.error("Get transceiver error:", err); + res.status(500).json({ success: false, error: "Internal server error" }); + } +}); diff --git a/packages/api/src/routes/vendors.ts b/packages/api/src/routes/vendors.ts new file mode 100644 index 0000000..825dfdd --- /dev/null +++ b/packages/api/src/routes/vendors.ts @@ -0,0 +1,15 @@ +import { Router, Request, Response } from "express"; +import { listVendors } from "../db/queries"; + +export const vendorRouter = Router(); + +// GET /api/vendors — List all vendors +vendorRouter.get("/", async (req: Request, res: Response) => { + try { + const vendors = await listVendors(req.query.type ? String(req.query.type) : undefined); + res.json({ success: true, data: vendors, total: vendors.length }); + } catch (err) { + console.error("List vendors error:", err); + res.status(500).json({ success: false, error: "Internal server error" }); + } +}); diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json new file mode 100644 index 0000000..1951778 --- /dev/null +++ b/packages/api/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000..1f6b395 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,45 @@ +{ + "name": "@tip/core", + "version": "1.0.0", + "description": "Core optical transceiver database. 159 products, 42 IEEE/MSA standards, 16 form factors, 9 speed tiers.", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc", + "prepublishOnly": "npm run build" + }, + "license": "MIT", + "keywords": [ + "transceiver", + "optics", + "sfp", + "qsfp", + "networking", + "fiber", + "ieee", + "telecom", + "osfp", + "qsfp-dd", + "optical", + "datacenter", + "100g", + "400g", + "800g" + ], + "files": [ + "dist", + "LICENSE", + "README.md" + ], + "repository": { + "type": "git", + "url": "https://github.com/renefichtmueller/transceiver-db" + }, + "author": "Rene Fichtmueller", + "engines": { + "node": ">=14" + }, + "devDependencies": { + "typescript": "^5.9.3" + } +} diff --git a/packages/core/src/breakouts.ts b/packages/core/src/breakouts.ts new file mode 100644 index 0000000..e3377ba --- /dev/null +++ b/packages/core/src/breakouts.ts @@ -0,0 +1,20 @@ +/** + * Breakout cable configurations. + * Maps high-speed ports to multiple lower-speed connections. + */ + +import type { Breakout } from "./types"; + +export const breakouts: readonly Breakout[] = [ + { id: "40g-4x10g-sr", from: "40GBASE-SR4", to: "4x 10GBASE-SR", formFactor: "QSFP+ to 4x SFP+", description: "Break out one 40G QSFP+ port into four 10G SFP+ ports. Uses MPO-to-LC harness.", cableType: "Passive", maxLength: "5m (passive) / 100m (active)", speedPerLane: "10G" }, + { id: "40g-4x10g-aoc", from: "40GBASE-SR4", to: "4x 10GBASE-SR", formFactor: "QSFP+ to 4x SFP+ AOC", description: "Active optical breakout cable from 40G QSFP+ to four 10G SFP+.", cableType: "Active", maxLength: "1-30m", speedPerLane: "10G" }, + { id: "100g-4x25g-sr", from: "100GBASE-SR4", to: "4x 25GBASE-SR", formFactor: "QSFP28 to 4x SFP28", description: "Break out one 100G QSFP28 port into four 25G SFP28 ports.", cableType: "Passive", maxLength: "5m (passive) / 100m (active)", speedPerLane: "25G" }, + { id: "100g-4x25g-dac", from: "100G QSFP28", to: "4x 25G SFP28", formFactor: "QSFP28 to 4x SFP28 DAC", description: "Passive copper breakout DAC from 100G QSFP28 to four 25G SFP28.", cableType: "Passive", maxLength: "1-5m", speedPerLane: "25G" }, + { id: "400g-4x100g-dr", from: "400GBASE-DR4", to: "4x 100GBASE-DR", formFactor: "QSFP-DD to 4x QSFP28 (MPO-12 to 4x LC)", description: "Break out one 400G DR4 port into four 100G DR ports. Parallel SMF to duplex LC.", cableType: "Passive", maxLength: "500m (fiber reach)", speedPerLane: "100G" }, + { id: "400g-4x100g-fr", from: "400GBASE-XDR4", to: "4x 100GBASE-FR1", formFactor: "QSFP-DD to 4x QSFP28", description: "Break out one 400G XDR4 port into four 100G FR1 ports. 2km reach per lane.", cableType: "Passive", maxLength: "2km (fiber reach)", speedPerLane: "100G" }, + { id: "400g-4x100g-lr", from: "400G-PLR4", to: "4x 100GBASE-LR1", formFactor: "QSFP-DD to 4x QSFP28", description: "Break out one 400G PLR4 port into four 100G LR1 ports. 10km reach per lane.", cableType: "Passive", maxLength: "10km (fiber reach)", speedPerLane: "100G" }, + { id: "800g-2x400g-dr", from: "800GBASE-DR8", to: "2x 400GBASE-DR4", formFactor: "OSFP to 2x QSFP-DD", description: "Break out one 800G DR8 port into two 400G DR4 ports.", cableType: "Passive", maxLength: "500m (fiber reach)", speedPerLane: "100G" }, + { id: "800g-8x100g-dr", from: "800GBASE-DR8", to: "8x 100GBASE-DR", formFactor: "OSFP to 8x QSFP28", description: "Break out one 800G DR8 port into eight 100G DR ports.", cableType: "Passive", maxLength: "500m (fiber reach)", speedPerLane: "100G" }, + { id: "200g-4x50g-sr", from: "200GBASE-SR4", to: "4x 50GBASE-SR", formFactor: "QSFP56 to 4x SFP56", description: "Break out one 200G SR4 port into four 50G SR ports.", cableType: "Passive", maxLength: "100m (OM4)", speedPerLane: "50G" }, + { id: "200g-2x100g-dr", from: "200GBASE-DR4", to: "2x 100GBASE-DR", formFactor: "QSFP56 to 2x QSFP28", description: "Break out one 200G DR4 port into two 100G DR ports.", cableType: "Passive", maxLength: "500m (fiber reach)", speedPerLane: "50G" }, +]; diff --git a/packages/core/src/database.ts b/packages/core/src/database.ts new file mode 100644 index 0000000..3ffebcc --- /dev/null +++ b/packages/core/src/database.ts @@ -0,0 +1,217 @@ +/** + * Transceiver product database — 159 optical transceiver products. + * Covers GBIC, XFP, SFP, SFP+, SFP28, SFP56, QSFP+, QSFP28, QSFP56, + * QSFP-DD, OSFP, CFP/CFP2/CFP4, CFP2-DCO, CXP, DAC, and AOC. + * + * Speeds: 1G, 10G, 25G, 40G, 50G, 100G, 200G, 400G, 800G. + * + * Data sourced from publicly available IEEE standards, MSA specifications, + * vendor datasheets, and industry documentation. + */ + +import type { Transceiver, VendorCompat } from "./types"; + +// Vendor shorthand helpers (internal) +const V_CISCO: VendorCompat = { vendor: "Cisco", partPattern: "SFP-*-*" }; +const V_JUNIPER: VendorCompat = { vendor: "Juniper", partPattern: "EX-SFP-*" }; +const V_ARISTA: VendorCompat = { vendor: "Arista", partPattern: "SFP-*-*" }; +const V_HUAWEI: VendorCompat = { vendor: "Huawei", partPattern: "SFP-*-*" }; +const V_NOKIA: VendorCompat = { vendor: "Nokia", partPattern: "3HE*" }; +const V_HPE: VendorCompat = { vendor: "HPE/Aruba", partPattern: "J*" }; +const V_DELL: VendorCompat = { vendor: "Dell", partPattern: "407-*" }; +const V_EXTREME: VendorCompat = { vendor: "Extreme", partPattern: "10*" }; +const V_NVIDIA: VendorCompat = { vendor: "NVIDIA", partPattern: "MMS4*|MMA1*" }; + +function allMajorVendors(): VendorCompat[] { + return [V_CISCO, V_JUNIPER, V_ARISTA, V_HUAWEI, V_NOKIA, V_HPE, V_DELL, V_EXTREME]; +} + +function dcVendors(): VendorCompat[] { + return [V_CISCO, V_JUNIPER, V_ARISTA, V_HUAWEI, V_DELL, V_EXTREME]; +} + +function carrierVendors(): VendorCompat[] { + return [V_CISCO, V_JUNIPER, V_HUAWEI, V_NOKIA]; +} + +/** + * Complete transceiver database — all 159 products. + */ +export const transceivers: readonly Transceiver[] = [ + // ── GBIC — 1G Legacy ── + { id: "gbic-sx", standard: "1000BASE-SX", formFactor: "GBIC", speed: "1G", speedGbps: 1, reachMeters: 550, reachLabel: "550m", fiberType: "MMF", wavelengths: "850nm", connector: "SC", powerConsumptionW: 1.5, tempRange: "COM", category: "Legacy", priceTier: "Budget", useCase: "Legacy 1G multimode short-reach links in older switches using GBIC slots.", vendors: [{ vendor: "Cisco", partPattern: "WS-G5484" }, { vendor: "Juniper", partPattern: "SRX-SFP-1GE-SX" }], tags: ["1G", "legacy", "multimode", "short-reach", "GBIC"] }, + { id: "gbic-lx", standard: "1000BASE-LX", formFactor: "GBIC", speed: "1G", speedGbps: 1, reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1310nm", connector: "SC", powerConsumptionW: 1.5, tempRange: "COM", category: "Legacy", priceTier: "Budget", useCase: "Legacy 1G single-mode links up to 10km in older GBIC-slot equipment.", vendors: [{ vendor: "Cisco", partPattern: "WS-G5486" }], tags: ["1G", "legacy", "singlemode", "10km", "GBIC"] }, + + // ── SFP — 1G ── + { id: "sfp-sx", standard: "1000BASE-SX", formFactor: "SFP", speed: "1G", speedGbps: 1, reachMeters: 550, reachLabel: "550m", fiberType: "MMF", wavelengths: "850nm", connector: "LC", powerConsumptionW: 0.8, tempRange: "COM", category: "DataCenter", priceTier: "Budget", useCase: "Standard 1G multimode short-reach for data center and campus backbone links.", vendors: [{ vendor: "Cisco", partPattern: "GLC-SX-MMD" }, { vendor: "Juniper", partPattern: "EX-SFP-1GE-SX" }, { vendor: "Arista", partPattern: "SFP-1G-SX" }, { vendor: "Huawei", partPattern: "SFP-GE-SX" }, V_NOKIA, V_HPE, V_DELL, V_EXTREME], tags: ["1G", "multimode", "short-reach", "campus", "SFP"] }, + { id: "sfp-lx", standard: "1000BASE-LX", formFactor: "SFP", speed: "1G", speedGbps: 1, reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1310nm", connector: "LC", powerConsumptionW: 0.8, tempRange: "COM", category: "Metro", priceTier: "Budget", useCase: "1G single-mode for campus/metro links up to 10km. Most common 1G SFP.", vendors: [{ vendor: "Cisco", partPattern: "GLC-LH-SMD" }, { vendor: "Juniper", partPattern: "EX-SFP-1GE-LX" }, { vendor: "Arista", partPattern: "SFP-1G-LX" }, { vendor: "Huawei", partPattern: "SFP-GE-LX-SM1310" }, V_NOKIA, V_HPE, V_DELL, V_EXTREME], tags: ["1G", "singlemode", "10km", "metro", "campus", "SFP"] }, + { id: "sfp-zx", standard: "1000BASE-ZX", formFactor: "SFP", speed: "1G", speedGbps: 1, reachMeters: 80000, reachLabel: "80km", fiberType: "SMF", wavelengths: "1550nm", connector: "LC", powerConsumptionW: 1.0, tempRange: "COM", category: "LongHaul", priceTier: "Standard", useCase: "1G long-haul single-mode up to 80km for inter-city or metro ring links.", vendors: [{ vendor: "Cisco", partPattern: "GLC-ZX-SMD" }, { vendor: "Juniper", partPattern: "SFP-1GE-LH" }, V_HUAWEI, V_NOKIA], tags: ["1G", "singlemode", "80km", "long-haul", "SFP"] }, + { id: "sfp-t", standard: "1000BASE-T", formFactor: "SFP", speed: "1G", speedGbps: 1, reachMeters: 100, reachLabel: "100m", fiberType: "Copper", wavelengths: "N/A", connector: "RJ45", powerConsumptionW: 1.0, tempRange: "COM", category: "Access", priceTier: "Budget", useCase: "1G copper SFP for short-reach RJ45 connections to servers or endpoints.", vendors: allMajorVendors(), tags: ["1G", "copper", "RJ45", "access", "SFP"] }, + { id: "sfp-bidi-1310-1550", standard: "1000BASE-BX10", formFactor: "SFP", speed: "1G", speedGbps: 1, reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1310nm TX / 1550nm RX", connector: "LC", powerConsumptionW: 0.8, tempRange: "COM", category: "BiDi", priceTier: "Standard", useCase: "Bidirectional 1G over a single fiber strand. Sold in pairs (upstream/downstream). Saves fiber.", vendors: [{ vendor: "Cisco", partPattern: "GLC-BX-U / GLC-BX-D" }, { vendor: "Juniper", partPattern: "SFP-1GE-BX" }, V_ARISTA, V_HUAWEI, V_NOKIA, V_HPE, V_DELL, V_EXTREME], tags: ["1G", "BiDi", "single-fiber", "singlemode", "10km", "SFP"] }, + { id: "sfp-cwdm-1470", standard: "SFP CWDM", formFactor: "SFP", speed: "1G", speedGbps: 1, reachMeters: 80000, reachLabel: "40-80km", fiberType: "SMF", wavelengths: "1470-1610nm (8 channels, 20nm spacing)", connector: "LC", powerConsumptionW: 1.0, tempRange: "COM", category: "CWDM", priceTier: "Standard", useCase: "1G CWDM for multiplexing up to 8 channels on a single fiber pair. Cost-effective WDM.", vendors: [{ vendor: "Cisco", partPattern: "CWDM-SFP-*" }, { vendor: "Juniper", partPattern: "SFP-1GE-CWDM*" }, V_HUAWEI, V_NOKIA], tags: ["1G", "CWDM", "WDM", "multiplexing", "SFP"] }, + + // ── XFP — 10G Legacy ── + { id: "xfp-sr", standard: "10GBASE-SR", formFactor: "XFP", speed: "10G", speedGbps: 10, reachMeters: 300, reachLabel: "300m", fiberType: "MMF", wavelengths: "850nm", connector: "LC", powerConsumptionW: 3.5, tempRange: "COM", category: "Legacy", priceTier: "Budget", useCase: "Legacy 10G short-reach for older platforms with XFP slots. Replaced by SFP+ in modern gear.", vendors: [{ vendor: "Cisco", partPattern: "XFP-10G-MM-SR" }, { vendor: "Juniper", partPattern: "XFP-10G-S" }], tags: ["10G", "legacy", "multimode", "short-reach", "XFP"] }, + { id: "xfp-lr", standard: "10GBASE-LR", formFactor: "XFP", speed: "10G", speedGbps: 10, reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1310nm", connector: "LC", powerConsumptionW: 3.5, tempRange: "COM", category: "Legacy", priceTier: "Budget", useCase: "Legacy 10G single-mode for metro links on older XFP-based routers.", vendors: [{ vendor: "Cisco", partPattern: "XFP-10GLR-OC192SR" }, { vendor: "Juniper", partPattern: "XFP-10G-L-OC192-SR1" }], tags: ["10G", "legacy", "singlemode", "10km", "XFP"] }, + { id: "xfp-er", standard: "10GBASE-ER", formFactor: "XFP", speed: "10G", speedGbps: 10, reachMeters: 40000, reachLabel: "40km", fiberType: "SMF", wavelengths: "1550nm", connector: "LC", powerConsumptionW: 3.5, tempRange: "COM", category: "Legacy", priceTier: "Standard", useCase: "Legacy 10G extended reach for metro/regional links on XFP platforms.", vendors: [{ vendor: "Cisco", partPattern: "XFP-10GER-OC192IR" }, { vendor: "Juniper", partPattern: "XFP-10GE-ER" }], tags: ["10G", "legacy", "singlemode", "40km", "XFP", "extended-reach"] }, + + // ── SFP+ — 10G ── + { id: "sfpp-sr", standard: "10GBASE-SR", formFactor: "SFP+", speed: "10G", speedGbps: 10, reachMeters: 300, reachLabel: "300m (OM3) / 400m (OM4)", fiberType: "MMF", wavelengths: "850nm", connector: "LC", powerConsumptionW: 1.0, tempRange: "COM", category: "DataCenter", priceTier: "Budget", useCase: "The workhorse of 10G data center connectivity. Short-reach multimode for server-to-switch links.", vendors: [{ vendor: "Cisco", partPattern: "SFP-10G-SR" }, { vendor: "Juniper", partPattern: "EX-SFP-10GE-SR" }, { vendor: "Arista", partPattern: "SFP-10G-SR" }, { vendor: "Huawei", partPattern: "SFP-10G-USR" }, V_NOKIA, V_HPE, V_DELL, V_EXTREME], tags: ["10G", "multimode", "short-reach", "data-center", "server", "SFP+"] }, + { id: "sfpp-lr", standard: "10GBASE-LR", formFactor: "SFP+", speed: "10G", speedGbps: 10, reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1310nm", connector: "LC", powerConsumptionW: 1.0, tempRange: "COM", category: "Metro", priceTier: "Budget", useCase: "10G single-mode for campus backbone and metro links up to 10km.", vendors: [{ vendor: "Cisco", partPattern: "SFP-10G-LR" }, { vendor: "Juniper", partPattern: "EX-SFP-10GE-LR" }, { vendor: "Arista", partPattern: "SFP-10G-LR" }, { vendor: "Huawei", partPattern: "SFP-10G-LR" }, V_NOKIA, V_HPE, V_DELL, V_EXTREME], tags: ["10G", "singlemode", "10km", "metro", "campus", "SFP+"] }, + { id: "sfpp-er", standard: "10GBASE-ER", formFactor: "SFP+", speed: "10G", speedGbps: 10, reachMeters: 40000, reachLabel: "40km", fiberType: "SMF", wavelengths: "1550nm", connector: "LC", powerConsumptionW: 1.5, tempRange: "COM", category: "Metro", priceTier: "Standard", useCase: "10G extended reach for metro rings and inter-building links up to 40km.", vendors: [{ vendor: "Cisco", partPattern: "SFP-10G-ER" }, { vendor: "Juniper", partPattern: "EX-SFP-10GE-ER" }, { vendor: "Arista", partPattern: "SFP-10G-ER" }, V_HUAWEI, V_NOKIA], tags: ["10G", "singlemode", "40km", "metro", "extended-reach", "SFP+"] }, + { id: "sfpp-zr", standard: "10GBASE-ZR", formFactor: "SFP+", speed: "10G", speedGbps: 10, reachMeters: 80000, reachLabel: "80km", fiberType: "SMF", wavelengths: "1550nm", connector: "LC", powerConsumptionW: 1.5, tempRange: "COM", category: "LongHaul", priceTier: "Standard", useCase: "10G long-reach for inter-city metro and regional network links up to 80km.", vendors: [{ vendor: "Cisco", partPattern: "SFP-10G-ZR" }, { vendor: "Juniper", partPattern: "SFP-10GE-ZR" }, V_HUAWEI, V_NOKIA], tags: ["10G", "singlemode", "80km", "long-haul", "SFP+"] }, + { id: "sfpp-bidi-10", standard: "10GBASE-BX", formFactor: "SFP+", speed: "10G", speedGbps: 10, reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1270nm TX / 1330nm RX", connector: "LC", powerConsumptionW: 1.0, tempRange: "COM", category: "BiDi", priceTier: "Standard", useCase: "Bidirectional 10G over single fiber. Pairs required (upstream/downstream). Halves fiber count.", vendors: [{ vendor: "Cisco", partPattern: "SFP-10G-BXU-I / SFP-10G-BXD-I" }, { vendor: "Juniper", partPattern: "SFP-10GE-BX" }, V_ARISTA, V_HUAWEI, V_NOKIA, V_HPE, V_DELL, V_EXTREME], tags: ["10G", "BiDi", "single-fiber", "singlemode", "10km", "SFP+"] }, + { id: "sfpp-cwdm", standard: "10G SFP+ CWDM", formFactor: "SFP+", speed: "10G", speedGbps: 10, reachMeters: 80000, reachLabel: "40-80km", fiberType: "SMF", wavelengths: "1270-1610nm (18 channels, 20nm spacing)", connector: "LC", powerConsumptionW: 1.2, tempRange: "COM", category: "CWDM", priceTier: "Standard", useCase: "10G CWDM SFP+ for WDM multiplexing. Up to 18 channels over a single fiber pair.", vendors: [{ vendor: "Cisco", partPattern: "CWDM-SFP10G-*" }, { vendor: "Juniper", partPattern: "SFP-10GE-CWDM*" }, V_HUAWEI, V_NOKIA], tags: ["10G", "CWDM", "WDM", "multiplexing", "SFP+"] }, + { id: "sfpp-dwdm", standard: "10G SFP+ DWDM", formFactor: "SFP+", speed: "10G", speedGbps: 10, reachMeters: 80000, reachLabel: "40-80km", fiberType: "SMF", wavelengths: "C-band (1528-1565nm, 80+ channels, 100GHz/50GHz spacing)", connector: "LC", powerConsumptionW: 1.5, tempRange: "COM", category: "DWDM", priceTier: "Premium", useCase: "10G DWDM for high-density wavelength multiplexing. 80+ channels on C-band. Fixed or tunable.", vendors: [{ vendor: "Cisco", partPattern: "DWDM-SFP10G-*" }, { vendor: "Juniper", partPattern: "SFP-10GE-DWDM*" }, V_HUAWEI, V_NOKIA], tags: ["10G", "DWDM", "WDM", "C-band", "tunable", "SFP+"] }, + { id: "sfpp-t", standard: "10GBASE-T", formFactor: "SFP+", speed: "10G", speedGbps: 10, reachMeters: 30, reachLabel: "30m", fiberType: "Copper", wavelengths: "N/A", connector: "RJ45", powerConsumptionW: 2.5, tempRange: "COM", category: "Access", priceTier: "Budget", useCase: "10G copper SFP+ for short-reach RJ45 connections. Higher power than fiber variants.", vendors: allMajorVendors(), tags: ["10G", "copper", "RJ45", "access", "SFP+"] }, + { id: "sfpp-usr", standard: "10GBASE-USR", ieeeReference: "SFF-8431", formFactor: "SFP+", speed: "10G", speedGbps: 10, lanes: 1, laneRate: "10.3125 Gbaud", modulation: "NRZ", reachMeters: 100, reachLabel: "10-100m (OM3/OM4)", fiberType: "MMF", wavelengths: "850nm", connector: "LC", powerConsumptionW: 1.0, tempRange: "COM", category: "DataCenter", priceTier: "Budget", useCase: "Ultra short-reach 10G for within-rack and adjacent-rack connections.", vendors: [{ vendor: "Cisco", partPattern: "SFP-10G-SR" }, { vendor: "Huawei", partPattern: "SFP-10G-USR" }], tags: ["10G", "multimode", "ultra-short-reach", "data-center", "SFP+"], generation: "Gen1 NRZ", marketStatus: "Mainstream" }, + { id: "sfpp-lrm", standard: "10GBASE-LRM", ieeeReference: "IEEE 802.3aq", formFactor: "SFP+", speed: "10G", speedGbps: 10, lanes: 1, laneRate: "10.3125 Gbaud", modulation: "NRZ", reachMeters: 220, reachLabel: "220m (FDDI/OM1 legacy MMF)", fiberType: "MMF", wavelengths: "1310nm", connector: "LC", powerConsumptionW: 1.5, tempRange: "COM", category: "DataCenter", priceTier: "Standard", useCase: "10G over legacy multimode fiber (OM1/OM2). Uses 1310nm to achieve 220m on older fiber.", vendors: [{ vendor: "Cisco", partPattern: "SFP-10G-LRM" }, { vendor: "Juniper", partPattern: "EX-SFP-10GE-LRM" }], tags: ["10G", "multimode", "legacy-fiber", "LRM", "SFP+"], generation: "Gen1 NRZ", marketStatus: "Legacy", yearIntroduced: 2006 }, + + // ── SFP28 — 25G ── + { id: "sfp28-sr", standard: "25GBASE-SR", ieeeReference: "IEEE 802.3by", formFactor: "SFP28", speed: "25G", speedGbps: 25, lanes: 1, laneRate: "25.78125 Gbaud", modulation: "NRZ", reachMeters: 100, reachLabel: "70m (OM3) / 100m (OM4)", fiberType: "MMF", wavelengths: "850nm", connector: "LC", powerConsumptionW: 1.0, tempRange: "COM", category: "DataCenter", priceTier: "Budget", useCase: "Standard 25G data center server access. Replaced 10G SFP+ in hyperscale deployments.", vendors: [{ vendor: "Cisco", partPattern: "SFP-25G-SR-S" }, { vendor: "Juniper", partPattern: "SFP-25G-SR" }, { vendor: "Arista", partPattern: "SFP-25G-SR" }, V_HUAWEI, V_NOKIA, V_HPE, V_DELL, V_EXTREME], tags: ["25G", "multimode", "short-reach", "data-center", "server", "SFP28"], generation: "Gen1 NRZ", marketStatus: "Mainstream", yearIntroduced: 2016 }, + { id: "sfp28-lr", standard: "25GBASE-LR", ieeeReference: "IEEE 802.3cc", formFactor: "SFP28", speed: "25G", speedGbps: 25, lanes: 1, laneRate: "25.78125 Gbaud", modulation: "NRZ", reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1310nm", connector: "LC", powerConsumptionW: 1.0, tempRange: "COM", category: "Metro", priceTier: "Standard", useCase: "25G single-mode for campus backbone and 5G fronthaul links up to 10km.", vendors: [{ vendor: "Cisco", partPattern: "SFP-25G-LR-S" }, { vendor: "Juniper", partPattern: "SFP-25G-LR" }, { vendor: "Arista", partPattern: "SFP-25G-LR" }, V_HUAWEI, V_NOKIA, V_HPE, V_DELL, V_EXTREME], tags: ["25G", "singlemode", "10km", "metro", "5G-fronthaul", "SFP28"], generation: "Gen1 NRZ", marketStatus: "Growth", yearIntroduced: 2017 }, + { id: "sfp28-er", standard: "25GBASE-ER", ieeeReference: "IEEE 802.3cc", formFactor: "SFP28", speed: "25G", speedGbps: 25, lanes: 1, laneRate: "25.78125 Gbaud", modulation: "NRZ", reachMeters: 30000, reachLabel: "30km", fiberType: "SMF", wavelengths: "1310nm", connector: "LC", powerConsumptionW: 1.5, tempRange: "COM", category: "Metro", priceTier: "Standard", useCase: "25G extended reach for metro and 5G midhaul applications.", vendors: [{ vendor: "Cisco", partPattern: "SFP-25G-ER-S" }, { vendor: "Juniper", partPattern: "SFP-25G-ER" }], tags: ["25G", "singlemode", "30km", "metro", "5G-midhaul", "SFP28"], generation: "Gen1 NRZ", marketStatus: "Growth", yearIntroduced: 2017 }, + { id: "sfp28-bidi", standard: "25GBASE-BX", formFactor: "SFP28", speed: "25G", speedGbps: 25, lanes: 1, modulation: "NRZ", reachMeters: 10000, reachLabel: "10-30km", fiberType: "SMF", wavelengths: "1270nm TX / 1330nm RX (or reverse)", connector: "LC", powerConsumptionW: 1.2, tempRange: "COM", category: "BiDi", priceTier: "Standard", useCase: "Bidirectional 25G over single fiber. Sold in pairs. Ideal for 5G fronthaul with limited fiber.", vendors: [{ vendor: "Cisco", partPattern: "SFP-25G-BX*" }, V_JUNIPER, V_ARISTA, V_HUAWEI, V_NOKIA, V_HPE, V_DELL, V_EXTREME], tags: ["25G", "BiDi", "single-fiber", "singlemode", "5G", "SFP28"], generation: "Gen1 NRZ", marketStatus: "Growth" }, + { id: "sfp28-cwdm", standard: "25G SFP28 CWDM", formFactor: "SFP28", speed: "25G", speedGbps: 25, lanes: 1, modulation: "NRZ", reachMeters: 40000, reachLabel: "10-40km", fiberType: "SMF", wavelengths: "1270-1610nm (CWDM channels, 20nm spacing)", connector: "LC", powerConsumptionW: 1.5, tempRange: "COM", category: "CWDM", priceTier: "Standard", useCase: "25G CWDM for wavelength multiplexing. Enables multiple 25G channels over a single fiber pair for 5G midhaul aggregation.", vendors: carrierVendors(), tags: ["25G", "CWDM", "WDM", "5G", "aggregation", "SFP28"], generation: "Gen1 NRZ", marketStatus: "Growth" }, + { id: "sfp28-lr-ind", standard: "25GBASE-LR Industrial", ieeeReference: "IEEE 802.3cc", formFactor: "SFP28", speed: "25G", speedGbps: 25, lanes: 1, modulation: "NRZ", reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1310nm", connector: "LC", powerConsumptionW: 1.2, tempRange: "IND", category: "5G", priceTier: "Premium", useCase: "Industrial temperature 25G SFP28 for 5G fronthaul in outdoor cabinets. Rated -40C to +85C.", vendors: carrierVendors(), tags: ["25G", "industrial", "outdoor", "5G", "fronthaul", "SFP28"], generation: "Gen1 NRZ", marketStatus: "Growth" }, + + // ── SFP56 — 50G ── + { id: "sfp56-sr", standard: "50GBASE-SR", ieeeReference: "IEEE 802.3cd", formFactor: "SFP56", speed: "50G", speedGbps: 50, lanes: 1, laneRate: "26.5625 Gbaud", modulation: "PAM4", reachMeters: 100, reachLabel: "70m (OM3) / 100m (OM4)", fiberType: "MMF", wavelengths: "850nm", connector: "LC", powerConsumptionW: 1.5, tempRange: "COM", category: "DataCenter", priceTier: "Standard", useCase: "50G single-lane for emerging high-density server access. Used in 200G-SR4 breakout scenarios.", vendors: dcVendors(), tags: ["50G", "multimode", "short-reach", "PAM4", "SFP56", "data-center"], generation: "Gen2 PAM4", marketStatus: "Growth", yearIntroduced: 2019 }, + { id: "sfp56-lr", standard: "50GBASE-LR", formFactor: "SFP56", speed: "50G", speedGbps: 50, reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1310nm", connector: "LC", powerConsumptionW: 1.5, tempRange: "COM", category: "Metro", priceTier: "Standard", useCase: "50G single-mode for high-bandwidth campus and metro links.", vendors: dcVendors(), tags: ["50G", "PAM4", "singlemode", "10km", "SFP56"] }, + + // ── QSFP+ — 40G ── + { id: "qsfpp-sr4", standard: "40GBASE-SR4", formFactor: "QSFP+", speed: "40G", speedGbps: 40, reachMeters: 150, reachLabel: "100m (OM3) / 150m (OM4)", fiberType: "MMF", wavelengths: "850nm", connector: "MPO-12", powerConsumptionW: 2.5, tempRange: "COM", category: "DataCenter", priceTier: "Budget", useCase: "Standard 40G multimode for data center spine-leaf links using MPO cabling.", vendors: [{ vendor: "Cisco", partPattern: "QSFP-40G-SR4" }, { vendor: "Juniper", partPattern: "QSFP-40G-SR4" }, { vendor: "Arista", partPattern: "QSFP-40G-SR4" }, { vendor: "Huawei", partPattern: "QSFP-40G-SR4" }, V_NOKIA, V_HPE, V_DELL, V_EXTREME], tags: ["40G", "multimode", "short-reach", "data-center", "MPO", "QSFP+"] }, + { id: "qsfpp-lr4", standard: "40GBASE-LR4", formFactor: "QSFP+", speed: "40G", speedGbps: 40, reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1271/1291/1311/1331nm (4 CWDM lanes)", connector: "LC", powerConsumptionW: 3.5, tempRange: "COM", category: "Metro", priceTier: "Standard", useCase: "40G single-mode for campus backbone and DCI links up to 10km using LC duplex fiber.", vendors: [{ vendor: "Cisco", partPattern: "QSFP-40G-LR4" }, { vendor: "Juniper", partPattern: "QSFP-40G-LR4" }, { vendor: "Arista", partPattern: "QSFP-40G-LR4" }, V_HUAWEI, V_NOKIA, V_HPE, V_DELL, V_EXTREME], tags: ["40G", "singlemode", "10km", "metro", "CWDM", "DCI", "QSFP+"] }, + { id: "qsfpp-er4", standard: "40GBASE-ER4", formFactor: "QSFP+", speed: "40G", speedGbps: 40, reachMeters: 40000, reachLabel: "40km", fiberType: "SMF", wavelengths: "1271/1291/1311/1331nm (4 CWDM lanes)", connector: "LC", powerConsumptionW: 3.5, tempRange: "COM", category: "Metro", priceTier: "Premium", useCase: "40G extended reach for metro and DCI links up to 40km.", vendors: carrierVendors(), tags: ["40G", "singlemode", "40km", "metro", "extended-reach", "QSFP+"] }, + { id: "qsfpp-sr-bidi", standard: "40GBASE-SR-BiDi", formFactor: "QSFP+", speed: "40G", speedGbps: 40, reachMeters: 150, reachLabel: "100m (OM3) / 150m (OM4)", fiberType: "MMF", wavelengths: "832nm / 918nm BiDi", connector: "LC", powerConsumptionW: 2.5, tempRange: "COM", category: "BiDi", priceTier: "Standard", useCase: "40G BiDi multimode over LC duplex. Enables 40G upgrade reusing existing 10G LC cabling.", vendors: [{ vendor: "Cisco", partPattern: "QSFP-40G-SR-BD" }, { vendor: "Arista", partPattern: "QSFP-40G-SRBD" }, V_JUNIPER, V_HUAWEI, V_DELL, V_EXTREME], tags: ["40G", "BiDi", "multimode", "LC-reuse", "data-center", "QSFP+"] }, + + // ── QSFP28 — 100G ── + { id: "qsfp28-sr4", standard: "100GBASE-SR4", formFactor: "QSFP28", speed: "100G", speedGbps: 100, reachMeters: 100, reachLabel: "70m (OM3) / 100m (OM4)", fiberType: "MMF", wavelengths: "850nm", connector: "MPO-12", powerConsumptionW: 3.5, tempRange: "COM", category: "DataCenter", priceTier: "Budget", useCase: "Standard 100G multimode for data center spine-leaf. The most deployed 100G optic. Breakout to 4x25G possible.", vendors: [{ vendor: "Cisco", partPattern: "QSFP-100G-SR4-S" }, { vendor: "Juniper", partPattern: "QSFP-100G-SR4" }, { vendor: "Arista", partPattern: "QSFP-100G-SR4" }, { vendor: "Huawei", partPattern: "QSFP-100G-SR4" }, V_NOKIA, V_HPE, V_DELL, V_EXTREME], tags: ["100G", "multimode", "short-reach", "data-center", "MPO", "breakout", "QSFP28", "IXP"] }, + { id: "qsfp28-sr1", standard: "100GBASE-SR1", ieeeReference: "IEEE 802.3cd", formFactor: "QSFP28", speed: "100G", speedGbps: 100, lanes: 1, laneRate: "53.125 Gbaud", modulation: "PAM4", reachMeters: 100, reachLabel: "70m (OM3) / 100m (OM4)", fiberType: "MMF", wavelengths: "850nm", connector: "LC", powerConsumptionW: 3.5, tempRange: "COM", category: "DataCenter", priceTier: "Budget", useCase: "100G single-lane over duplex LC MMF. Uses PAM4 modulation. Simpler and cheaper than SR4.", vendors: dcVendors(), tags: ["100G", "multimode", "short-reach", "PAM4", "duplex-LC", "QSFP28"], generation: "Gen2 PAM4", marketStatus: "Growth", yearIntroduced: 2018 }, + { id: "qsfp28-sr2", standard: "100GBASE-SR2", ieeeReference: "100G Lambda MSA", formFactor: "QSFP28", speed: "100G", speedGbps: 100, lanes: 2, laneRate: "26.5625 Gbaud", modulation: "PAM4", reachMeters: 100, reachLabel: "100m (OM4)", fiberType: "MMF", wavelengths: "850nm", connector: "LC", powerConsumptionW: 3.5, tempRange: "COM", category: "DataCenter", priceTier: "Budget", useCase: "100G SR2 uses 2x50G PAM4 over duplex LC. No MPO needed. Popular in DCs moving to duplex fiber.", vendors: dcVendors(), tags: ["100G", "multimode", "duplex-LC", "short-reach", "data-center", "PAM4", "QSFP28"], generation: "Gen2 PAM4", marketStatus: "Mainstream", yearIntroduced: 2018 }, + { id: "qsfp28-dr1", standard: "100GBASE-DR1", formFactor: "QSFP28", speed: "100G", speedGbps: 100, reachMeters: 500, reachLabel: "500m", fiberType: "SMF", wavelengths: "1310nm", connector: "LC", powerConsumptionW: 3.5, tempRange: "COM", category: "DataCenter", priceTier: "Standard", useCase: "100G single-lane single-mode for intra-campus data center. Uses PAM4 modulation.", vendors: [{ vendor: "Cisco", partPattern: "QSFP-100G-DR-S" }, { vendor: "Arista", partPattern: "QSFP-100G-DR" }, V_JUNIPER, V_HUAWEI, V_DELL, V_EXTREME], tags: ["100G", "singlemode", "500m", "data-center", "PAM4", "single-lane", "QSFP28"] }, + { id: "qsfp28-fr1", standard: "100GBASE-FR1", ieeeReference: "IEEE 802.3cu", formFactor: "QSFP28", speed: "100G", speedGbps: 100, lanes: 1, laneRate: "53.125 Gbaud", modulation: "PAM4", reachMeters: 2000, reachLabel: "2km", fiberType: "SMF", wavelengths: "1310nm", connector: "LC", powerConsumptionW: 3.5, tempRange: "COM", category: "DCI", priceTier: "Standard", useCase: "100G single-lambda for 2km campus DCI. Interworks with 400GBASE-FR4 breakout.", vendors: [{ vendor: "Cisco", partPattern: "QSFP-100G-FR-S" }, { vendor: "Juniper", partPattern: "QSFP-100G-FR" }, { vendor: "Arista", partPattern: "QSFP-100G-FR" }, V_HUAWEI, V_NOKIA, V_HPE, V_DELL, V_EXTREME], tags: ["100G", "singlemode", "2km", "DCI", "campus", "PAM4", "QSFP28"], generation: "Gen2 PAM4", marketStatus: "Growth", yearIntroduced: 2021 }, + { id: "qsfp28-lr1", standard: "100GBASE-LR1", ieeeReference: "IEEE 802.3cu", formFactor: "QSFP28", speed: "100G", speedGbps: 100, lanes: 1, laneRate: "53.125 Gbaud", modulation: "PAM4", reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1310nm", connector: "LC", powerConsumptionW: 3.5, tempRange: "COM", category: "IXP", priceTier: "Standard", useCase: "100G single-lambda for 10km. The new IXP standard (100G LR-1) replacing LR4 at major exchanges.", vendors: [{ vendor: "Cisco", partPattern: "QSFP-100G-LR1-S" }, { vendor: "Juniper", partPattern: "QSFP-100G-LR1" }, { vendor: "Nokia", partPattern: "3HE*" }, V_ARISTA, V_HUAWEI, V_HPE, V_DELL, V_EXTREME], tags: ["100G", "singlemode", "10km", "IXP", "LR-1", "PAM4", "QSFP28"], generation: "Gen2 PAM4", marketStatus: "Growth", yearIntroduced: 2021 }, + { id: "qsfp28-lr4", standard: "100GBASE-LR4", formFactor: "QSFP28", speed: "100G", speedGbps: 100, reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1295/1300/1305/1310nm (4 LAN-WDM lanes)", connector: "LC", powerConsumptionW: 4.5, tempRange: "COM", category: "Metro", priceTier: "Standard", useCase: "100G single-mode for metro DCI and campus backbone up to 10km. Uses 4x25G LAN-WDM lanes.", vendors: [{ vendor: "Cisco", partPattern: "QSFP-100G-LR4-S" }, { vendor: "Juniper", partPattern: "QSFP-100G-LR4" }, { vendor: "Arista", partPattern: "QSFP-100G-LR4" }, { vendor: "Huawei", partPattern: "QSFP-100G-LR4" }, V_NOKIA, V_HPE, V_DELL, V_EXTREME], tags: ["100G", "singlemode", "10km", "metro", "DCI", "LAN-WDM", "QSFP28", "IXP"] }, + { id: "qsfp28-cwdm4", standard: "100G CWDM4", formFactor: "QSFP28", speed: "100G", speedGbps: 100, reachMeters: 2000, reachLabel: "2km", fiberType: "SMF", wavelengths: "1271/1291/1311/1331nm (4 CWDM lanes)", connector: "LC", powerConsumptionW: 3.5, tempRange: "COM", category: "DCI", priceTier: "Budget", useCase: "100G CWDM4 for short-reach DCI up to 2km. Lower cost than LR4 for inter-building links.", vendors: [{ vendor: "Cisco", partPattern: "QSFP-100G-CWDM4-S" }, { vendor: "Juniper", partPattern: "QSFP-100G-CWDM4" }, { vendor: "Arista", partPattern: "QSFP-100G-CWDM4" }, V_HUAWEI, V_DELL, V_EXTREME], tags: ["100G", "CWDM4", "singlemode", "2km", "DCI", "cost-effective", "QSFP28"] }, + { id: "qsfp28-er4", standard: "100GBASE-ER4", formFactor: "QSFP28", speed: "100G", speedGbps: 100, reachMeters: 40000, reachLabel: "40km", fiberType: "SMF", wavelengths: "1295/1300/1305/1310nm (4 LAN-WDM lanes)", connector: "LC", powerConsumptionW: 4.5, tempRange: "COM", category: "Metro", priceTier: "Premium", useCase: "100G extended reach for metro rings and longer DCI links up to 40km.", vendors: [{ vendor: "Cisco", partPattern: "QSFP-100G-ER4L" }, { vendor: "Juniper", partPattern: "QSFP-100G-ER4" }, V_HUAWEI, V_NOKIA], tags: ["100G", "singlemode", "40km", "metro", "extended-reach", "QSFP28"] }, + { id: "qsfp28-zr4", standard: "100GBASE-ZR4", formFactor: "QSFP28", speed: "100G", speedGbps: 100, reachMeters: 80000, reachLabel: "80km", fiberType: "SMF", wavelengths: "1296/1300/1305/1309nm (4 LAN-WDM lanes)", connector: "LC", powerConsumptionW: 5.0, tempRange: "COM", category: "LongHaul", priceTier: "Premium", useCase: "100G long-haul for regional networks up to 80km without amplification.", vendors: carrierVendors(), tags: ["100G", "singlemode", "80km", "long-haul", "regional", "QSFP28"] }, + { id: "qsfp28-lr8", standard: "100G LR8", formFactor: "QSFP28", speed: "100G", speedGbps: 100, reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "8 CWDM wavelengths (1271-1411nm)", connector: "LC", powerConsumptionW: 4.0, tempRange: "COM", category: "Metro", priceTier: "Standard", useCase: "100G using 8 CWDM lanes at 12.5G each. For platforms that do not support 25G-per-lane optics.", vendors: carrierVendors(), tags: ["100G", "singlemode", "10km", "CWDM", "8-lane", "legacy-platform", "QSFP28"] }, + { id: "qsfp28-psm4", standard: "100G PSM4", formFactor: "QSFP28", speed: "100G", speedGbps: 100, reachMeters: 500, reachLabel: "500m", fiberType: "SMF", wavelengths: "1310nm (4 parallel fibers)", connector: "MPO-12", powerConsumptionW: 3.5, tempRange: "COM", category: "DataCenter", priceTier: "Budget", useCase: "100G parallel single-mode for intra-DC links up to 500m. MPO-12 connector.", vendors: dcVendors(), tags: ["100G", "singlemode", "500m", "parallel", "MPO", "data-center", "QSFP28"] }, + { id: "qsfp28-sr-bidi", standard: "100GBASE-SR BiDi", formFactor: "QSFP28", speed: "100G", speedGbps: 100, reachMeters: 100, reachLabel: "70m (OM3) / 100m (OM4)", fiberType: "MMF", wavelengths: "832nm / 918nm BiDi (PAM4)", connector: "LC", powerConsumptionW: 3.5, tempRange: "COM", category: "BiDi", priceTier: "Standard", useCase: "100G BiDi over LC duplex multimode. Enables 100G upgrade reusing existing 10G/25G LC MMF cabling.", vendors: [{ vendor: "Cisco", partPattern: "QSFP-100G-SR1.2" }, { vendor: "Arista", partPattern: "QSFP-100G-SRBD" }], tags: ["100G", "BiDi", "multimode", "LC-reuse", "data-center", "QSFP28"] }, + { id: "qsfp28-dwdm", standard: "100G QSFP28 DWDM Tunable", formFactor: "QSFP28", speed: "100G", speedGbps: 100, reachMeters: 80000, reachLabel: "80km", fiberType: "SMF", wavelengths: "C-band tunable (1528-1565nm)", connector: "LC", powerConsumptionW: 5.0, tempRange: "COM", category: "DWDM", priceTier: "Premium", useCase: "100G DWDM tunable for metro/regional WDM networks. Integrates with ROADM systems.", vendors: carrierVendors(), tags: ["100G", "DWDM", "tunable", "C-band", "metro", "ROADM", "QSFP28"] }, + { id: "qsfp28-zr-coherent", standard: "100GBASE-ZR (Coherent)", ieeeReference: "IEEE 802.3ct", formFactor: "QSFP28", speed: "100G", speedGbps: 100, lanes: 1, laneRate: "~64 Gbaud", modulation: "DP-QPSK (coherent)", reachMeters: 80000, reachLabel: "80km+ (DWDM amplified)", fiberType: "SMF", wavelengths: "C-band (tunable, DWDM)", connector: "LC", powerConsumptionW: 5.0, tempRange: "COM", category: "Coherent", priceTier: "Premium", useCase: "100G coherent pluggable for DWDM long-haul. Phase/amplitude modulation with coherent detection.", vendors: carrierVendors(), tags: ["100G", "coherent", "DWDM", "long-haul", "C-band", "tunable", "QSFP28"], generation: "Coherent", marketStatus: "Mainstream", yearIntroduced: 2021 }, + + // ── CXP — 100G/120G Legacy ── + { id: "cxp-sr10", standard: "100GBASE-SR10", formFactor: "CXP", speed: "100G", speedGbps: 100, reachMeters: 150, reachLabel: "100m (OM3) / 150m (OM4)", fiberType: "MMF", wavelengths: "850nm (10 lanes x 10G)", connector: "MPO-24", powerConsumptionW: 6.0, tempRange: "COM", category: "Legacy", priceTier: "Standard", useCase: "Legacy 100G/120G CXP form factor for InfiniBand and early 100G Ethernet.", vendors: [{ vendor: "Cisco", partPattern: "CXP-100G-SR10" }], tags: ["100G", "120G", "legacy", "InfiniBand", "CXP", "multimode"] }, + + // ── CFP / CFP2 / CFP4 — 100G ── + { id: "cfp-lr4", standard: "100GBASE-LR4 CFP", formFactor: "CFP", speed: "100G", speedGbps: 100, reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1295/1300/1305/1310nm", connector: "LC", powerConsumptionW: 24.0, tempRange: "COM", category: "Legacy", priceTier: "Standard", useCase: "First-generation 100G CFP form factor. Large footprint, being replaced by CFP2/QSFP28.", vendors: carrierVendors(), tags: ["100G", "singlemode", "10km", "legacy", "CFP"] }, + { id: "cfp2-lr4", standard: "100GBASE-LR4 CFP2", formFactor: "CFP2", speed: "100G", speedGbps: 100, reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1295/1300/1305/1310nm", connector: "LC", powerConsumptionW: 9.0, tempRange: "COM", category: "Metro", priceTier: "Standard", useCase: "100G CFP2 for carrier/service provider routers with CFP2 slots.", vendors: carrierVendors(), tags: ["100G", "singlemode", "10km", "carrier", "CFP2"] }, + { id: "cfp4-lr4", standard: "100GBASE-LR4 CFP4", formFactor: "CFP4", speed: "100G", speedGbps: 100, reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1295/1300/1305/1310nm", connector: "LC", powerConsumptionW: 6.0, tempRange: "COM", category: "Metro", priceTier: "Standard", useCase: "Compact 100G CFP4 for high-density router line cards.", vendors: carrierVendors(), tags: ["100G", "singlemode", "10km", "carrier", "high-density", "CFP4"] }, + + // ── CFP2-DCO — Coherent 100G-400G ── + { id: "cfp2dco-100g", standard: "100G CFP2-DCO Coherent", formFactor: "CFP2-DCO", speed: "100G", speedGbps: 100, reachMeters: 2000000, reachLabel: "2000km+", fiberType: "SMF", wavelengths: "C-band tunable (1528-1565nm)", connector: "LC", powerConsumptionW: 18.0, tempRange: "COM", category: "Coherent", priceTier: "Premium", useCase: "100G coherent for long-haul and submarine links. DP-QPSK modulation.", vendors: carrierVendors(), tags: ["100G", "coherent", "DP-QPSK", "long-haul", "submarine", "DWDM", "tunable", "CFP2-DCO"] }, + { id: "cfp2dco-200g", standard: "200G CFP2-DCO Coherent", formFactor: "CFP2-DCO", speed: "200G", speedGbps: 200, reachMeters: 1000000, reachLabel: "1000km+", fiberType: "SMF", wavelengths: "C-band tunable (1528-1565nm)", connector: "LC", powerConsumptionW: 20.0, tempRange: "COM", category: "Coherent", priceTier: "Premium", useCase: "200G coherent for long-haul transport. DP-16QAM or DP-QPSK modulation.", vendors: carrierVendors(), tags: ["200G", "coherent", "DP-16QAM", "long-haul", "DWDM", "tunable", "CFP2-DCO"] }, + { id: "cfp2dco-400g", standard: "400G CFP2-DCO Coherent", formFactor: "CFP2-DCO", speed: "400G", speedGbps: 400, reachMeters: 600000, reachLabel: "600km+", fiberType: "SMF", wavelengths: "C-band tunable (1528-1565nm)", connector: "LC", powerConsumptionW: 22.0, tempRange: "COM", category: "Coherent", priceTier: "Premium", useCase: "400G coherent for long-haul and DCI. DP-16QAM modulation at high baud rate.", vendors: carrierVendors(), tags: ["400G", "coherent", "DP-16QAM", "long-haul", "DCI", "DWDM", "tunable", "CFP2-DCO"] }, + + // ── QSFP56 — 200G ── + { id: "qsfp56-sr4", standard: "200GBASE-SR4", ieeeReference: "IEEE 802.3cd", formFactor: "QSFP56", speed: "200G", speedGbps: 200, lanes: 4, laneRate: "26.5625 Gbaud", modulation: "PAM4", reachMeters: 100, reachLabel: "70m (OM3) / 100m (OM4)", fiberType: "MMF", wavelengths: "850nm (4x50G PAM4)", connector: "MPO-12", powerConsumptionW: 7.0, tempRange: "COM", category: "DataCenter", priceTier: "Standard", useCase: "200G multimode for data center links. 4 lanes at 50G PAM4 each.", vendors: [{ vendor: "Cisco", partPattern: "QSFP-200G-SR4" }, { vendor: "Arista", partPattern: "QSFP-200G-SR4" }, V_JUNIPER, V_HUAWEI, V_DELL, V_EXTREME], tags: ["200G", "multimode", "PAM4", "data-center", "QSFP56"], generation: "Gen2 PAM4", marketStatus: "Mainstream", yearIntroduced: 2019, breakoutCapable: true, breakoutTo: "4x50GBASE-SR or 2x100GBASE-SR2" }, + { id: "qsfp56-dr4", standard: "200GBASE-DR4", ieeeReference: "IEEE 802.3cd", formFactor: "QSFP56", speed: "200G", speedGbps: 200, lanes: 4, laneRate: "26.5625 Gbaud", modulation: "PAM4", reachMeters: 500, reachLabel: "500m", fiberType: "SMF", wavelengths: "1310nm (4x50G PAM4 parallel)", connector: "MPO-12", powerConsumptionW: 7.0, tempRange: "COM", category: "DataCenter", priceTier: "Standard", useCase: "200G parallel single-mode for intra-DC links up to 500m. Breakout to 4x50G possible.", vendors: dcVendors(), tags: ["200G", "singlemode", "500m", "parallel", "breakout", "data-center", "QSFP56"], generation: "Gen2 PAM4", marketStatus: "Mainstream", yearIntroduced: 2019, breakoutCapable: true, breakoutTo: "2x100GBASE-DR or 4x50G" }, + { id: "qsfp56-fr4", standard: "200GBASE-FR4", ieeeReference: "IEEE 802.3cu", formFactor: "QSFP56", speed: "200G", speedGbps: 200, lanes: 4, laneRate: "26.5625 Gbaud", modulation: "PAM4", reachMeters: 2000, reachLabel: "2km", fiberType: "SMF", wavelengths: "1271/1291/1311/1331nm (CWDM4)", connector: "LC", powerConsumptionW: 7.0, tempRange: "COM", category: "DCI", priceTier: "Standard", useCase: "200G for short-reach DCI up to 2km. Uses CWDM wavelengths over LC duplex fiber.", vendors: dcVendors(), tags: ["200G", "CWDM", "singlemode", "2km", "DCI", "QSFP56"], generation: "Gen2 PAM4", marketStatus: "Mainstream", yearIntroduced: 2021 }, + { id: "qsfp56-lr4", standard: "200GBASE-LR4", ieeeReference: "IEEE 802.3cu", formFactor: "QSFP56", speed: "200G", speedGbps: 200, lanes: 4, laneRate: "26.5625 Gbaud", modulation: "PAM4", reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1295/1300/1305/1310nm (4 LAN-WDM lanes x 50G)", connector: "LC", powerConsumptionW: 8.0, tempRange: "COM", category: "Metro", priceTier: "Premium", useCase: "200G metro DCI links up to 10km using LAN-WDM.", vendors: carrierVendors(), tags: ["200G", "singlemode", "10km", "metro", "DCI", "LAN-WDM", "QSFP56"], generation: "Gen2 PAM4", marketStatus: "Mainstream", yearIntroduced: 2021 }, + + // ── QSFP-DD — 400G ── + { id: "qsfpdd-sr8", standard: "400GBASE-SR8", formFactor: "QSFP-DD", speed: "400G", speedGbps: 400, reachMeters: 100, reachLabel: "70m (OM3) / 100m (OM4)", fiberType: "MMF", wavelengths: "850nm (8x50G PAM4)", connector: "MPO-16", powerConsumptionW: 12.0, tempRange: "COM", category: "DataCenter", priceTier: "Standard", useCase: "400G multimode for short-reach data center links. 8 lanes at 50G PAM4.", vendors: [{ vendor: "Cisco", partPattern: "QDD-400G-SR8" }, { vendor: "Arista", partPattern: "QDD-400G-SR8" }, V_JUNIPER, V_HUAWEI, V_DELL, V_EXTREME], tags: ["400G", "multimode", "short-reach", "data-center", "MPO-16", "PAM4", "QSFP-DD"] }, + { id: "qsfpdd-sr4-2", standard: "400GBASE-SR4.2", ieeeReference: "IEEE 802.3cm", formFactor: "QSFP-DD", speed: "400G", speedGbps: 400, lanes: 4, laneRate: "53.125 Gbaud", modulation: "PAM4", reachMeters: 100, reachLabel: "100m (OM4)", fiberType: "MMF", wavelengths: "850nm + 910nm (bidirectional)", connector: "MPO-12", powerConsumptionW: 12.0, tempRange: "COM", category: "DataCenter", priceTier: "Standard", useCase: "400G BiDi over MPO-12 using two wavelengths. Saves fiber vs SR8.", vendors: dcVendors(), tags: ["400G", "multimode", "BiDi", "data-center", "MPO-12", "cabling-reuse", "QSFP-DD"], generation: "Gen2 PAM4", marketStatus: "Mainstream", yearIntroduced: 2020, breakoutCapable: true, breakoutTo: "2x200GBASE-SR4 or 4x100GBASE-SR" }, + { id: "qsfpdd-dr4", standard: "400GBASE-DR4", formFactor: "QSFP-DD", speed: "400G", speedGbps: 400, reachMeters: 500, reachLabel: "500m", fiberType: "SMF", wavelengths: "1310nm (4x100G PAM4 parallel)", connector: "MPO-12", powerConsumptionW: 12.0, tempRange: "COM", category: "DataCenter", priceTier: "Standard", useCase: "400G parallel single-mode for intra-DC. Breakout to 4x100G DR1 possible. Key DCI building block.", vendors: [{ vendor: "Cisco", partPattern: "QDD-400G-DR4-S" }, { vendor: "Juniper", partPattern: "QDD-400G-DR4" }, { vendor: "Arista", partPattern: "QDD-400G-DR4" }, V_HUAWEI, V_DELL, V_EXTREME], tags: ["400G", "singlemode", "500m", "parallel", "breakout", "data-center", "DCI", "QSFP-DD"] }, + { id: "qsfpdd-fr4", standard: "400GBASE-FR4", formFactor: "QSFP-DD", speed: "400G", speedGbps: 400, reachMeters: 2000, reachLabel: "2km", fiberType: "SMF", wavelengths: "1271/1291/1311/1331nm (4 CWDM lanes x 100G)", connector: "LC", powerConsumptionW: 12.0, tempRange: "COM", category: "DCI", priceTier: "Standard", useCase: "400G FR4 for DCI up to 2km using CWDM4 over LC duplex. Most popular 400G DCI optic.", vendors: [{ vendor: "Cisco", partPattern: "QDD-400G-FR4-S" }, { vendor: "Juniper", partPattern: "QDD-400G-FR4" }, { vendor: "Arista", partPattern: "QDD-400G-FR4" }, V_HUAWEI, V_NOKIA, V_HPE, V_DELL, V_EXTREME], tags: ["400G", "CWDM", "singlemode", "2km", "DCI", "popular", "QSFP-DD"] }, + { id: "qsfpdd-lr4", standard: "400GBASE-LR4", formFactor: "QSFP-DD", speed: "400G", speedGbps: 400, reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1295/1300/1305/1310nm (4 LAN-WDM lanes x 100G)", connector: "LC", powerConsumptionW: 14.0, tempRange: "COM", category: "Metro", priceTier: "Premium", useCase: "400G for metro DCI and campus backbone up to 10km.", vendors: [{ vendor: "Cisco", partPattern: "QDD-400G-LR4-S" }, { vendor: "Juniper", partPattern: "QDD-400G-LR4" }, V_HUAWEI, V_NOKIA], tags: ["400G", "singlemode", "10km", "metro", "DCI", "LAN-WDM", "QSFP-DD"] }, + { id: "qsfpdd-lr8", standard: "400GBASE-LR8", formFactor: "QSFP-DD", speed: "400G", speedGbps: 400, reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "8 CWDM wavelengths (1271-1411nm, 8x50G)", connector: "LC", powerConsumptionW: 14.0, tempRange: "COM", category: "Metro", priceTier: "Premium", useCase: "400G LR8 using 8 CWDM lanes at 50G each. For platforms with 50G-per-lane electronics.", vendors: carrierVendors(), tags: ["400G", "CWDM8", "singlemode", "10km", "metro", "8-lane", "QSFP-DD"] }, + { id: "qsfpdd-er4", standard: "400GBASE-ER4", formFactor: "QSFP-DD", speed: "400G", speedGbps: 400, reachMeters: 40000, reachLabel: "40km", fiberType: "SMF", wavelengths: "1295/1300/1305/1310nm (4 LAN-WDM lanes, amplified)", connector: "LC", powerConsumptionW: 16.0, tempRange: "COM", category: "Metro", priceTier: "Premium", useCase: "400G extended reach for metro rings up to 40km.", vendors: carrierVendors(), tags: ["400G", "singlemode", "40km", "metro", "extended-reach", "SOA", "QSFP-DD"] }, + { id: "qsfpdd-xdr4", standard: "400GBASE-XDR4 (4x100G-FR)", formFactor: "QSFP-DD", speed: "400G", speedGbps: 400, lanes: 4, laneRate: "53.125 Gbaud", modulation: "PAM4", reachMeters: 2000, reachLabel: "2km", fiberType: "SMF", wavelengths: "1310nm", connector: "MPO-12", powerConsumptionW: 12.0, tempRange: "COM", category: "DCI", priceTier: "Standard", useCase: "400G XDR4 = 4 parallel SMF lanes at 100G each for 2km. Breaks out to 4x100G-FR1.", vendors: [{ vendor: "Arista", partPattern: "QDD-400G-XDR4" }, { vendor: "Juniper", partPattern: "QDD-4X100G-FR" }, V_CISCO, V_HUAWEI, V_DELL, V_EXTREME], tags: ["400G", "singlemode", "2km", "parallel-SMF", "DCI", "breakout", "PAM4", "QSFP-DD"], generation: "Gen2 PAM4", marketStatus: "Growth", breakoutCapable: true, breakoutTo: "4x100GBASE-FR1" }, + { id: "qsfpdd-plr4", standard: "400G-PLR4 (4x100G-LR)", formFactor: "QSFP-DD", speed: "400G", speedGbps: 400, lanes: 4, laneRate: "53.125 Gbaud", modulation: "PAM4", reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1310nm", connector: "MPO-12", powerConsumptionW: 14.0, tempRange: "COM", category: "Metro", priceTier: "Premium", useCase: "400G parallel-LR for 10km. 4 parallel SMF lanes. Breaks out to 4x100G-LR1.", vendors: [{ vendor: "Arista", partPattern: "QDD-400G-PLR4" }, { vendor: "Juniper", partPattern: "QDD-400G-PLR4" }, V_CISCO, V_HUAWEI], tags: ["400G", "singlemode", "10km", "parallel-SMF", "metro", "breakout", "QSFP-DD"], generation: "Gen2 PAM4", marketStatus: "Growth", breakoutCapable: true, breakoutTo: "4x100GBASE-LR1" }, + { id: "qsfpdd-zr", standard: "400G-ZR (OIF)", formFactor: "QSFP-DD", speed: "400G", speedGbps: 400, reachMeters: 120000, reachLabel: "120km (unamplified)", fiberType: "SMF", wavelengths: "C-band tunable (1528-1565nm), DP-16QAM", connector: "LC", powerConsumptionW: 18.0, tempRange: "COM", category: "Coherent", priceTier: "Premium", useCase: "400G-ZR coherent in QSFP-DD form factor. Industry standard (OIF) for DCI up to 120km unamplified.", vendors: [{ vendor: "Cisco", partPattern: "QDD-400G-ZR-S" }, { vendor: "Juniper", partPattern: "QDD-400G-ZR" }, { vendor: "Arista", partPattern: "QDD-400G-ZR" }, V_HUAWEI, V_NOKIA], tags: ["400G", "coherent", "ZR", "DP-16QAM", "DCI", "120km", "OIF", "pluggable", "QSFP-DD"] }, + { id: "qsfpdd-zrp", standard: "400G-ZR+ (OpenZR+)", formFactor: "QSFP-DD", speed: "400G", speedGbps: 400, reachMeters: 2000000, reachLabel: "500km+ (amplified)", fiberType: "SMF", wavelengths: "C-band tunable, flexible modulation (QPSK/8QAM/16QAM)", connector: "LC", powerConsumptionW: 20.0, tempRange: "COM", category: "Coherent", priceTier: "Premium", useCase: "400G-ZR+ coherent with flexible modulation for metro-to-long-haul. OpenZR+ MSA standard.", vendors: [{ vendor: "Cisco", partPattern: "QDD-400G-ZRP-S" }, { vendor: "Juniper", partPattern: "QDD-400G-ZRP" }, { vendor: "Arista", partPattern: "QDD-400G-ZRP" }, V_HUAWEI, V_NOKIA], tags: ["400G", "coherent", "ZR+", "OpenZR+", "flexible-modulation", "metro", "long-haul", "DCI", "pluggable", "QSFP-DD"] }, + + // ── OSFP — 400G / 800G ── + { id: "osfp-sr8", standard: "400GBASE-SR8 OSFP", formFactor: "OSFP", speed: "400G", speedGbps: 400, reachMeters: 100, reachLabel: "100m (OM4)", fiberType: "MMF", wavelengths: "850nm (8x50G PAM4)", connector: "MPO-16", powerConsumptionW: 15.0, tempRange: "COM", category: "DataCenter", priceTier: "Standard", useCase: "400G multimode OSFP for platforms with OSFP cages.", vendors: [{ vendor: "Arista", partPattern: "OSFP-400G-SR8" }, V_CISCO, V_JUNIPER, V_HUAWEI, V_DELL, V_EXTREME], tags: ["400G", "multimode", "data-center", "OSFP"] }, + { id: "osfp-dr4", standard: "400GBASE-DR4 OSFP", formFactor: "OSFP", speed: "400G", speedGbps: 400, reachMeters: 500, reachLabel: "500m", fiberType: "SMF", wavelengths: "1310nm (4x100G PAM4)", connector: "MPO-12", powerConsumptionW: 15.0, tempRange: "COM", category: "DataCenter", priceTier: "Standard", useCase: "400G parallel single-mode OSFP for next-gen data center fabrics.", vendors: dcVendors(), tags: ["400G", "singlemode", "500m", "data-center", "OSFP"] }, + { id: "osfp-fr4", standard: "400GBASE-FR4 OSFP", formFactor: "OSFP", speed: "400G", speedGbps: 400, reachMeters: 2000, reachLabel: "2km", fiberType: "SMF", wavelengths: "1271/1291/1311/1331nm (4 CWDM x 100G)", connector: "LC", powerConsumptionW: 15.0, tempRange: "COM", category: "DCI", priceTier: "Standard", useCase: "400G FR4 in OSFP form factor for DCI links up to 2km.", vendors: dcVendors(), tags: ["400G", "CWDM", "singlemode", "2km", "DCI", "OSFP"] }, + { id: "osfp-800g-sr8", standard: "800GBASE-SR8", formFactor: "OSFP", speed: "800G", speedGbps: 800, reachMeters: 50, reachLabel: "30m (OM3) / 50m (OM4)", fiberType: "MMF", wavelengths: "850nm (8x100G PAM4)", connector: "MPO-16", powerConsumptionW: 22.0, tempRange: "COM", category: "DataCenter", priceTier: "Premium", useCase: "800G multimode for AI/ML GPU cluster interconnects in hyperscale data centers.", vendors: [{ vendor: "Cisco", partPattern: "OSFP-800G-SR8" }, { vendor: "Arista", partPattern: "OSFP-800G-SR8" }], tags: ["800G", "multimode", "data-center", "AI", "GPU", "hyperscale", "OSFP"] }, + { id: "osfp-800g-dr8", standard: "800GBASE-DR8", formFactor: "OSFP", speed: "800G", speedGbps: 800, reachMeters: 500, reachLabel: "500m", fiberType: "SMF", wavelengths: "1310nm (8x100G PAM4 parallel)", connector: "MPO-16", powerConsumptionW: 22.0, tempRange: "COM", category: "DataCenter", priceTier: "Premium", useCase: "800G parallel single-mode for hyperscale DC fabrics. Breakout to 2x400G or 8x100G possible.", vendors: [{ vendor: "Cisco", partPattern: "OSFP-800G-DR8" }, { vendor: "Arista", partPattern: "OSFP-800G-DR8" }], tags: ["800G", "singlemode", "500m", "parallel", "breakout", "hyperscale", "data-center", "OSFP"] }, + { id: "osfp-800g-2fr4", standard: "800G-2FR4", formFactor: "OSFP", speed: "800G", speedGbps: 800, reachMeters: 2000, reachLabel: "2km", fiberType: "SMF", wavelengths: "8 CWDM wavelengths (4 per fiber direction)", connector: "CS", powerConsumptionW: 22.0, tempRange: "COM", category: "DCI", priceTier: "Premium", useCase: "800G for DCI up to 2km using duplex CS connector. Dual FR4 in a single module.", vendors: dcVendors(), tags: ["800G", "CWDM", "singlemode", "2km", "DCI", "CS-connector", "OSFP"] }, + { id: "osfp-800g-zr", standard: "800G-ZR", formFactor: "OSFP", speed: "800G", speedGbps: 800, reachMeters: 120000, reachLabel: "80-120km", fiberType: "SMF", wavelengths: "C-band tunable, DP-64QAM/DP-16QAM", connector: "LC", powerConsumptionW: 25.0, tempRange: "COM", category: "Coherent", priceTier: "Premium", useCase: "800G coherent pluggable for DCI. Next-gen after 400G-ZR.", vendors: [{ vendor: "Cisco", partPattern: "OSFP-800G-ZR" }, { vendor: "Arista", partPattern: "OSFP-800G-ZR" }, V_JUNIPER, V_HUAWEI, V_NOKIA], tags: ["800G", "coherent", "ZR", "DCI", "pluggable", "next-gen", "OSFP"] }, + + // ── DAC — Direct Attach Cables ── + { id: "dac-sfpp-1m", standard: "10G SFP+ DAC", formFactor: "SFP+", speed: "10G", speedGbps: 10, reachMeters: 5, reachLabel: "1-5m", fiberType: "Copper", wavelengths: "N/A", connector: "None", powerConsumptionW: 0.5, tempRange: "COM", category: "DAC", priceTier: "Budget", useCase: "10G passive copper DAC for in-rack server-to-switch links. Lowest cost and latency option.", vendors: allMajorVendors(), tags: ["10G", "DAC", "copper", "passive", "in-rack", "low-latency", "SFP+"] }, + { id: "dac-sfp28-3m", standard: "25G SFP28 DAC", formFactor: "SFP28", speed: "25G", speedGbps: 25, reachMeters: 5, reachLabel: "1-5m", fiberType: "Copper", wavelengths: "N/A", connector: "None", powerConsumptionW: 0.5, tempRange: "COM", category: "DAC", priceTier: "Budget", useCase: "25G passive copper DAC for in-rack 25G server connections.", vendors: allMajorVendors(), tags: ["25G", "DAC", "copper", "passive", "in-rack", "leaf-spine", "SFP28"] }, + { id: "dac-qsfpp-3m", standard: "40G QSFP+ DAC", formFactor: "QSFP+", speed: "40G", speedGbps: 40, reachMeters: 5, reachLabel: "1-5m", fiberType: "Copper", wavelengths: "N/A", connector: "None", powerConsumptionW: 0.5, tempRange: "COM", category: "DAC", priceTier: "Budget", useCase: "40G passive copper DAC for spine-to-leaf and storage links within a rack.", vendors: allMajorVendors(), tags: ["40G", "DAC", "copper", "passive", "in-rack", "QSFP+"] }, + { id: "dac-qsfp28-3m", standard: "100G QSFP28 DAC", formFactor: "QSFP28", speed: "100G", speedGbps: 100, reachMeters: 5, reachLabel: "1-5m", fiberType: "Copper", wavelengths: "N/A", connector: "None", powerConsumptionW: 1.0, tempRange: "COM", category: "DAC", priceTier: "Budget", useCase: "100G passive copper DAC for spine-leaf and storage interconnects within racks.", vendors: allMajorVendors(), tags: ["100G", "DAC", "copper", "passive", "in-rack", "QSFP28"] }, + { id: "dac-qsfpdd-3m", standard: "400G QSFP-DD DAC", formFactor: "QSFP-DD", speed: "400G", speedGbps: 400, reachMeters: 3, reachLabel: "1-3m", fiberType: "Copper", wavelengths: "N/A", connector: "None", powerConsumptionW: 1.5, tempRange: "COM", category: "DAC", priceTier: "Budget", useCase: "400G passive copper DAC for in-rack high-bandwidth interconnects.", vendors: dcVendors(), tags: ["400G", "DAC", "copper", "passive", "in-rack", "QSFP-DD"] }, + { id: "dac-osfp-800g", standard: "800G OSFP DAC", formFactor: "OSFP", speed: "800G", speedGbps: 800, reachMeters: 2, reachLabel: "1-2m", fiberType: "Copper", wavelengths: "N/A", connector: "None", powerConsumptionW: 2.0, tempRange: "COM", category: "DAC", priceTier: "Standard", useCase: "800G DAC for GPU-to-switch and AI cluster interconnects. Shortest latency option for 800G.", vendors: [{ vendor: "Cisco", partPattern: "OSFP-800G-CU*" }, { vendor: "Arista", partPattern: "OSFP-800G-DAC*" }], tags: ["800G", "DAC", "copper", "AI", "GPU", "OSFP"] }, + + // ── AOC — Active Optical Cables ── + { id: "aoc-sfpp-10m", standard: "10G SFP+ AOC", formFactor: "SFP+", speed: "10G", speedGbps: 10, reachMeters: 100, reachLabel: "1-100m", fiberType: "MMF", wavelengths: "850nm (embedded)", connector: "None", powerConsumptionW: 1.0, tempRange: "COM", category: "AOC", priceTier: "Budget", useCase: "10G AOC for inter-rack links beyond DAC reach. Lighter than copper.", vendors: allMajorVendors(), tags: ["10G", "AOC", "multimode", "inter-rack", "SFP+"] }, + { id: "aoc-qsfp28-30m", standard: "100G QSFP28 AOC", formFactor: "QSFP28", speed: "100G", speedGbps: 100, reachMeters: 100, reachLabel: "1-100m", fiberType: "MMF", wavelengths: "850nm (embedded)", connector: "None", powerConsumptionW: 3.0, tempRange: "COM", category: "AOC", priceTier: "Budget", useCase: "100G AOC for inter-rack DC links. Lighter and cheaper than SR4 + MPO patch cords.", vendors: allMajorVendors(), tags: ["100G", "AOC", "multimode", "inter-rack", "data-center", "QSFP28"] }, + { id: "aoc-qsfpdd-30m", standard: "400G QSFP-DD AOC", formFactor: "QSFP-DD", speed: "400G", speedGbps: 400, reachMeters: 100, reachLabel: "1-100m", fiberType: "MMF", wavelengths: "850nm (embedded)", connector: "None", powerConsumptionW: 10.0, tempRange: "COM", category: "AOC", priceTier: "Standard", useCase: "400G AOC for short inter-rack links in high-density data centers.", vendors: dcVendors(), tags: ["400G", "AOC", "multimode", "inter-rack", "QSFP-DD"] }, + + // ── Industrial Temperature Variants ── + { id: "sfpp-lr-ind", standard: "10GBASE-LR Industrial", formFactor: "SFP+", speed: "10G", speedGbps: 10, reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1310nm", connector: "LC", powerConsumptionW: 1.2, tempRange: "IND", category: "Access", priceTier: "Standard", useCase: "10G industrial-temp (-40 to +85C) for outdoor deployments, telecom shelters, and cell towers.", vendors: allMajorVendors(), tags: ["10G", "industrial", "outdoor", "telecom", "cell-tower", "SFP+"] }, + { id: "sfp-lx-ind", standard: "1000BASE-LX Industrial", formFactor: "SFP", speed: "1G", speedGbps: 1, reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1310nm", connector: "LC", powerConsumptionW: 1.0, tempRange: "IND", category: "Access", priceTier: "Standard", useCase: "1G industrial-temp for outdoor access networks, utility SCADA, and harsh environments.", vendors: allMajorVendors(), tags: ["1G", "industrial", "outdoor", "SCADA", "utility", "SFP"] }, + { id: "qsfp28-lr4-ind", standard: "100GBASE-LR4 Industrial", formFactor: "QSFP28", speed: "100G", speedGbps: 100, reachMeters: 10000, reachLabel: "10km", fiberType: "SMF", wavelengths: "1295/1300/1305/1310nm", connector: "LC", powerConsumptionW: 5.0, tempRange: "IND", category: "Metro", priceTier: "Premium", useCase: "100G industrial-temp for telecom outdoor cabinets and cell-site aggregation.", vendors: carrierVendors(), tags: ["100G", "industrial", "outdoor", "telecom", "QSFP28"] }, +]; + +// ── Search & Filter Functions ── + +/** + * Search transceivers by any keyword. Searches across standard, form factor, + * speed, use case, tags, and vendor names. + */ +export function searchTransceivers(query: string): Transceiver[] { + const q = query.toLowerCase(); + return transceivers.filter( + (t) => + t.standard.toLowerCase().includes(q) || + t.formFactor.toLowerCase().includes(q) || + t.speed.toLowerCase().includes(q) || + t.useCase.toLowerCase().includes(q) || + t.category.toLowerCase().includes(q) || + t.wavelengths.toLowerCase().includes(q) || + t.tags.some((tag) => tag.toLowerCase().includes(q)) || + t.vendors.some((v) => v.vendor.toLowerCase().includes(q)) || + (t.modulation && t.modulation.toLowerCase().includes(q)) || + (t.generation && t.generation.toLowerCase().includes(q)) + ); +} + +/** Filter by form factor (e.g., "SFP+", "QSFP-DD", "OSFP"). */ +export function getByFormFactor(formFactor: string): Transceiver[] { + return transceivers.filter( + (t) => t.formFactor.toLowerCase() === formFactor.toLowerCase() + ); +} + +/** Filter by speed tier (e.g., "10G", "100G", "400G", "800G"). */ +export function getBySpeed(speed: string): Transceiver[] { + return transceivers.filter( + (t) => t.speed.toLowerCase() === speed.toLowerCase() + ); +} + +/** Filter by maximum reach in meters. Returns transceivers that reach at least `minMeters`. */ +export function getByReach(minMeters: number): Transceiver[] { + return transceivers.filter((t) => t.reachMeters >= minMeters); +} + +/** Filter by product category (e.g., "DataCenter", "Coherent", "DAC"). */ +export function getByCategory(category: string): Transceiver[] { + return transceivers.filter( + (t) => t.category.toLowerCase() === category.toLowerCase() + ); +} + +/** Get a single transceiver by its unique ID. */ +export function getById(id: string): Transceiver | undefined { + return transceivers.find((t) => t.id === id); +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 0000000..18aaa19 --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,37 @@ +/** + * transceiver-db — Open-source optical transceiver database + * + * 159 products, 42 IEEE/MSA standards, 16 form factors, 9 speed tiers. + * From 1G SFP to 800G OSFP. Zero dependencies. + */ + +export { + transceivers, + searchTransceivers, + getByFormFactor, + getBySpeed, + getByReach, + getByCategory, + getById, +} from "./database"; + +export { standards, getStandard, searchStandards } from "./standards"; + +export { competitors, getCompetitor } from "./market"; + +export { breakouts } from "./breakouts"; + +export type { + Transceiver, + Standard, + Competitor, + Breakout, + FormFactor, + FiberType, + ConnectorType, + TempRange, + ProductCategory, + PriceTier, + MarketStatus, + VendorCompat, +} from "./types"; diff --git a/packages/core/src/market.ts b/packages/core/src/market.ts new file mode 100644 index 0000000..af0eefb --- /dev/null +++ b/packages/core/src/market.ts @@ -0,0 +1,28 @@ +/** + * Competitor landscape — neutral industry data. + * No vendor bias. No sales language. + */ + +import type { Competitor } from "./types"; + +export const competitors: readonly Competitor[] = [ + { name: "Cisco", type: "OEM", headquarters: "San Jose, USA", marketPosition: "Largest networking vendor globally. Sells branded optics. Acquired Acacia Communications for coherent technology.", formFactorsOffered: ["SFP", "SFP+", "SFP28", "QSFP+", "QSFP28", "QSFP-DD", "OSFP", "CFP2-DCO"], speedTiersOffered: ["1G", "10G", "25G", "40G", "100G", "400G", "800G"], strengths: ["Dominant installed base", "End-to-end solution", "TAC support tied to branded optics"], weaknesses: ["Significant price premium over compatible optics", "Vendor lock-in practices"] }, + { name: "Juniper Networks", type: "OEM", headquarters: "Sunnyvale, USA (acquired by HPE 2024)", marketPosition: "Strong in service provider and large enterprise. MX/PTX series for routing, QFX/EX for switching.", formFactorsOffered: ["SFP", "SFP+", "SFP28", "QSFP+", "QSFP28", "QSFP-DD", "CFP2-DCO"], speedTiersOffered: ["1G", "10G", "25G", "40G", "100G", "400G"], strengths: ["Junos OS reliability", "Strong SP/carrier presence", "PTX for massive scale"], weaknesses: ["Optics premium over compatible alternatives", "Smaller market share than Cisco"] }, + { name: "Arista Networks", type: "OEM", headquarters: "Santa Clara, USA", marketPosition: "Dominant in hyperscale data centers and cloud. Largest DC switching vendor by port volume. Leader in 400G/800G deployments.", formFactorsOffered: ["SFP+", "SFP28", "QSFP+", "QSFP28", "QSFP-DD", "OSFP"], speedTiersOffered: ["10G", "25G", "40G", "100G", "400G", "800G"], strengths: ["EOS software quality", "Hyperscale dominance", "Early 800G adoption", "Liquid-cooled optics for AI"], weaknesses: ["Optics premium over compatible alternatives", "Primarily DC focused"] }, + { name: "Huawei", type: "OEM", headquarters: "Shenzhen, China", marketPosition: "Largest carrier/SP equipment vendor globally. Strong in EMEA, APAC, Middle East. CloudEngine for DC switching.", formFactorsOffered: ["SFP", "SFP+", "SFP28", "QSFP+", "QSFP28", "QSFP-DD", "OSFP", "CFP2-DCO"], speedTiersOffered: ["1G", "10G", "25G", "40G", "100G", "400G", "800G"], strengths: ["Aggressive pricing vs Western OEMs", "Massive carrier installed base", "Strong in coherent/DWDM"], weaknesses: ["Geopolitical restrictions in some markets"] }, + { name: "Nokia", type: "OEM", headquarters: "Espoo, Finland", marketPosition: "Major carrier/SP vendor. 7750 SR series for routing, 7250 IXR for DC. Strong in IXP infrastructure.", formFactorsOffered: ["SFP", "SFP+", "SFP28", "QSFP+", "QSFP28", "QSFP-DD", "CFP2-DCO"], speedTiersOffered: ["1G", "10G", "25G", "40G", "100G", "400G", "800G"], strengths: ["IXP platform dominance", "FP5 silicon for high-performance routing", "Strong coherent optics"], weaknesses: ["Optics premium", "Smaller DC switching presence"] }, + { name: "FLEXOPTIX", type: "Compatible", headquarters: "Darmstadt, Germany", marketPosition: "Premium compatible optics vendor with unique FlexBox hardware programmer. Codes transceivers on-site for any vendor. 300+ supported switch/router vendors. Lifetime warranty. Strong in service provider, IXP, and enterprise markets across EMEA.", formFactorsOffered: ["SFP", "SFP+", "SFP28", "SFP56", "QSFP+", "QSFP28", "QSFP56", "QSFP-DD", "OSFP", "CFP", "CFP2", "CFP4", "CFP2-DCO", "XFP", "CXP"], speedTiersOffered: ["1G", "10G", "25G", "40G", "50G", "100G", "200G", "400G", "800G"], strengths: ["FlexBox: on-site hardware programmer — recode any transceiver for any vendor in seconds", "300+ supported vendors (largest compatibility matrix in the industry)", "Multi-vendor infrastructure support — one transceiver works across Cisco, Juniper, Arista, Nokia, Huawei and 295+ more", "Lifetime warranty on all products", "Technical support by network engineers, not call center scripts", "Same-day shipping from German warehouse", "Strong NOG/peering community presence"], weaknesses: ["Not the cheapest option (premium quality positioning)", "European-focused logistics (expanding globally)"] }, + { name: "FS.COM", type: "Whitebox", headquarters: "Wilmington, USA (manufacturing in China)", marketPosition: "Major compatible optics vendor. Massive online catalog. Tests compatibility with Cisco, Arista, Juniper, NVIDIA.", formFactorsOffered: ["SFP", "SFP+", "SFP28", "SFP56", "QSFP+", "QSFP28", "QSFP56", "QSFP-DD", "OSFP"], speedTiersOffered: ["1G", "10G", "25G", "40G", "50G", "100G", "200G", "400G", "800G", "1.6T"], strengths: ["Aggressive pricing", "Huge catalog", "Online ordering", "NVIDIA InfiniBand compatibility"], weaknesses: ["Pre-coded only — no field recoding", "Limited field customization", "Quality varies by batch", "No equivalent to FlexBox programmer"] }, + { name: "Innolight Technology", type: "Manufacturer", headquarters: "Suzhou, China", marketPosition: "Top optical transceiver manufacturer by revenue. Primary supplier to NVIDIA and major hyperscalers. Dominates 800G market.", formFactorsOffered: ["SFP28", "QSFP28", "QSFP56", "QSFP-DD", "OSFP", "OSFP-XD"], speedTiersOffered: ["25G", "100G", "200G", "400G", "800G", "1.6T"], strengths: ["Primary supplier to NVIDIA", "Massive manufacturing scale", "Silicon photonics leader", "800G/1.6T early adopter"], weaknesses: ["Primarily sells to hyperscalers/OEMs", "Not direct to enterprise"] }, + { name: "Coherent Corp", type: "Manufacturer", headquarters: "Saxonburg, USA", marketPosition: "Top-tier transceiver manufacturer (formerly II-VI/Finisar). Vertically integrated. Strong in coherent optics and telecom.", formFactorsOffered: ["SFP+", "SFP28", "QSFP28", "QSFP-DD", "OSFP", "CFP2-DCO", "OSFP-XD"], speedTiersOffered: ["10G", "25G", "100G", "400G", "800G", "1.6T"], strengths: ["Vertical integration", "Coherent optics leadership", "InP and SiPh capabilities"], weaknesses: ["Premium pricing", "OEM/hyperscale focus"] }, + { name: "Broadcom", type: "Manufacturer", headquarters: "San Jose, USA", marketPosition: "Major silicon + optics vendor. Tomahawk switch ASICs plus transceiver modules. Co-packaged optics initiative.", formFactorsOffered: ["SFP+", "SFP28", "QSFP28", "QSFP-DD", "OSFP"], speedTiersOffered: ["10G", "25G", "100G", "400G", "800G"], strengths: ["Switch ASIC + optics synergy", "Co-packaged optics leadership"], weaknesses: ["Premium pricing", "Mostly hyperscale/OEM channel"] }, + { name: "HPE/Aruba", type: "OEM", headquarters: "Houston, USA", marketPosition: "Strong in campus/enterprise networking. Aruba CX for modern campus. Juniper acquisition expanding portfolio.", formFactorsOffered: ["SFP", "SFP+", "SFP28", "QSFP+", "QSFP28", "QSFP-DD"], speedTiersOffered: ["1G", "10G", "25G", "40G", "100G", "400G"], strengths: ["Campus/enterprise dominance", "Aruba CX modern OS", "Juniper integration expanding"], weaknesses: ["Optics premium", "Less DC presence (pre-Juniper)"] }, + { name: "Dell Technologies", type: "OEM", headquarters: "Round Rock, USA", marketPosition: "PowerSwitch for DC switching. Strong server attach rate driving optics demand.", formFactorsOffered: ["SFP", "SFP+", "SFP28", "QSFP+", "QSFP28", "QSFP-DD"], speedTiersOffered: ["1G", "10G", "25G", "40G", "100G", "400G"], strengths: ["Server + switch bundle deals", "OS10/FTOS flexibility", "Open networking friendly"], weaknesses: ["Optics premium", "Smaller networking market share"] }, + { name: "Extreme Networks", type: "OEM", headquarters: "Morrisville, USA", marketPosition: "Consolidated multiple brands (Brocade, Avaya Networking, Enterasys, Aerohive). Strong in campus and education.", formFactorsOffered: ["SFP", "SFP+", "SFP28", "QSFP+", "QSFP28"], speedTiersOffered: ["1G", "10G", "25G", "40G", "100G"], strengths: ["Campus/education market", "Fabric Connect technology", "Unified management"], weaknesses: ["Optics premium", "Complex legacy product lines"] }, +]; + +/** Find a competitor by name (partial match). */ +export function getCompetitor(name: string): Competitor | undefined { + const q = name.toLowerCase(); + return competitors.find((c) => c.name.toLowerCase().includes(q)); +} diff --git a/packages/core/src/standards.ts b/packages/core/src/standards.ts new file mode 100644 index 0000000..9697de5 --- /dev/null +++ b/packages/core/src/standards.ts @@ -0,0 +1,82 @@ +/** + * IEEE 802.3 and MSA standards reference — 42 standards. + */ + +import type { Standard } from "./types"; + +export const standards: readonly Standard[] = [ + // 1G + { standard: "1000BASE-SX", ieeeReference: "IEEE 802.3z", speed: "1G", lanes: 1, laneRate: "1.25 Gbaud", modulation: "NRZ", fiberType: "MMF (OM1-OM4)", wavelength: "850nm", maxReachMeters: 550, maxReachLabel: "220m (OM1) / 550m (OM2+)", connector: "LC/SC", fecRequired: false, formFactors: ["SFP", "GBIC"], yearRatified: 1998, notes: "Original Gigabit Ethernet multimode standard." }, + { standard: "1000BASE-LX", ieeeReference: "IEEE 802.3z", speed: "1G", lanes: 1, laneRate: "1.25 Gbaud", modulation: "NRZ", fiberType: "SMF", wavelength: "1310nm", maxReachMeters: 10000, maxReachLabel: "10km", connector: "LC/SC", fecRequired: false, formFactors: ["SFP", "GBIC"], yearRatified: 1998, notes: "Standard 1G single-mode. Also works on MMF with mode conditioning patch cable." }, + { standard: "1000BASE-ZX", ieeeReference: "Vendor-defined (not IEEE)", speed: "1G", lanes: 1, laneRate: "1.25 Gbaud", modulation: "NRZ", fiberType: "SMF", wavelength: "1550nm", maxReachMeters: 80000, maxReachLabel: "70-80km", connector: "LC", fecRequired: false, formFactors: ["SFP"], yearRatified: 0, notes: "Not an IEEE standard. Vendor-defined. Uses 1550nm for extended reach." }, + { standard: "1000BASE-T", ieeeReference: "IEEE 802.3ab", speed: "1G", lanes: 4, laneRate: "250 Mbaud", modulation: "PAM5", fiberType: "Copper (Cat5e+)", wavelength: "N/A", maxReachMeters: 100, maxReachLabel: "100m", connector: "RJ45", fecRequired: false, formFactors: ["SFP"], yearRatified: 1999, notes: "Gigabit over copper. SFP form factor draws ~1W." }, + { standard: "1000BASE-BX10", ieeeReference: "IEEE 802.3ah", speed: "1G", lanes: 1, laneRate: "1.25 Gbaud", modulation: "NRZ", fiberType: "SMF (single fiber)", wavelength: "1310/1490nm", maxReachMeters: 10000, maxReachLabel: "10km", connector: "LC", fecRequired: false, formFactors: ["SFP"], yearRatified: 2004, notes: "Bidirectional over single fiber strand. Sold in pairs." }, + + // 10G + { standard: "10GBASE-SR", ieeeReference: "IEEE 802.3ae", speed: "10G", lanes: 1, laneRate: "10.3125 Gbaud", modulation: "NRZ", fiberType: "MMF (OM3/OM4)", wavelength: "850nm", maxReachMeters: 400, maxReachLabel: "300m (OM3) / 400m (OM4)", connector: "LC", fecRequired: false, formFactors: ["SFP+", "XFP"], yearRatified: 2002, notes: "Most deployed 10G optic worldwide." }, + { standard: "10GBASE-LR", ieeeReference: "IEEE 802.3ae", speed: "10G", lanes: 1, laneRate: "10.3125 Gbaud", modulation: "NRZ", fiberType: "SMF", wavelength: "1310nm", maxReachMeters: 10000, maxReachLabel: "10km", connector: "LC", fecRequired: false, formFactors: ["SFP+", "XFP"], yearRatified: 2002, notes: "Standard 10G single-mode. Backbone of campus and metro networks." }, + { standard: "10GBASE-ER", ieeeReference: "IEEE 802.3ae", speed: "10G", lanes: 1, laneRate: "10.3125 Gbaud", modulation: "NRZ", fiberType: "SMF", wavelength: "1550nm", maxReachMeters: 40000, maxReachLabel: "40km", connector: "LC", fecRequired: false, formFactors: ["SFP+", "XFP"], yearRatified: 2002, notes: "Extended reach 10G for metro rings and inter-city links." }, + { standard: "10GBASE-ZR", ieeeReference: "Vendor-defined (not IEEE)", speed: "10G", lanes: 1, laneRate: "10.3125 Gbaud", modulation: "NRZ", fiberType: "SMF", wavelength: "1550nm", maxReachMeters: 80000, maxReachLabel: "80km", connector: "LC", fecRequired: false, formFactors: ["SFP+", "XFP"], yearRatified: 0, notes: "Not an IEEE standard. Vendor-defined. 80km reach with high-power laser." }, + { standard: "10GBASE-T", ieeeReference: "IEEE 802.3an", speed: "10G", lanes: 4, laneRate: "2.5 Gbaud", modulation: "PAM16/DSQ128", fiberType: "Copper (Cat6a/Cat7)", wavelength: "N/A", maxReachMeters: 100, maxReachLabel: "100m (Cat6a)", connector: "RJ45", fecRequired: false, formFactors: ["SFP+"], yearRatified: 2006, notes: "10G over copper. 30m on Cat6, 100m on Cat6a." }, + { standard: "10GBASE-LRM", ieeeReference: "IEEE 802.3aq", speed: "10G", lanes: 1, laneRate: "10.3125 Gbaud", modulation: "NRZ", fiberType: "MMF (legacy OM1/OM2)", wavelength: "1310nm", maxReachMeters: 220, maxReachLabel: "220m", connector: "LC", fecRequired: false, formFactors: ["SFP+"], yearRatified: 2006, notes: "10G over legacy multimode fiber." }, + + // 25G + { standard: "25GBASE-SR", ieeeReference: "IEEE 802.3by", speed: "25G", lanes: 1, laneRate: "25.78125 Gbaud", modulation: "NRZ", fiberType: "MMF (OM3/OM4)", wavelength: "850nm", maxReachMeters: 100, maxReachLabel: "70m (OM3) / 100m (OM4)", connector: "LC", fecRequired: true, formFactors: ["SFP28"], yearRatified: 2016, notes: "Standard 25G data center server access." }, + { standard: "25GBASE-LR", ieeeReference: "IEEE 802.3cc", speed: "25G", lanes: 1, laneRate: "25.78125 Gbaud", modulation: "NRZ", fiberType: "SMF", wavelength: "1310nm", maxReachMeters: 10000, maxReachLabel: "10km", connector: "LC", fecRequired: false, formFactors: ["SFP28"], yearRatified: 2017, notes: "25G single-mode for campus/metro. Critical for 5G fronthaul (eCPRI)." }, + { standard: "25GBASE-ER", ieeeReference: "IEEE 802.3cc", speed: "25G", lanes: 1, laneRate: "25.78125 Gbaud", modulation: "NRZ", fiberType: "SMF", wavelength: "1310nm", maxReachMeters: 30000, maxReachLabel: "30km", connector: "LC", fecRequired: true, formFactors: ["SFP28"], yearRatified: 2017, notes: "Extended reach 25G for metro and 5G midhaul." }, + + // 40G + { standard: "40GBASE-SR4", ieeeReference: "IEEE 802.3ba", speed: "40G", lanes: 4, laneRate: "10.3125 Gbaud", modulation: "NRZ", fiberType: "MMF (OM3/OM4)", wavelength: "850nm", maxReachMeters: 150, maxReachLabel: "100m (OM3) / 150m (OM4)", connector: "MPO-12", fecRequired: false, formFactors: ["QSFP+"], yearRatified: 2010, notes: "4x10G parallel optics. Can break out to 4x10GBASE-SR." }, + { standard: "40GBASE-LR4", ieeeReference: "IEEE 802.3ba", speed: "40G", lanes: 4, laneRate: "10.3125 Gbaud", modulation: "NRZ", fiberType: "SMF", wavelength: "1310nm (4 CWDM wavelengths)", maxReachMeters: 10000, maxReachLabel: "10km", connector: "LC", fecRequired: false, formFactors: ["QSFP+"], yearRatified: 2010, notes: "4 CWDM wavelengths over duplex LC." }, + { standard: "40GBASE-ER4", ieeeReference: "IEEE 802.3bm", speed: "40G", lanes: 4, laneRate: "10.3125 Gbaud", modulation: "NRZ", fiberType: "SMF", wavelength: "1310nm (4 CWDM wavelengths)", maxReachMeters: 40000, maxReachLabel: "40km", connector: "LC", fecRequired: false, formFactors: ["QSFP+"], yearRatified: 2015, notes: "Extended reach 40G for metro ring and DCI." }, + + // 100G + { standard: "100GBASE-SR4", ieeeReference: "IEEE 802.3bm", speed: "100G", lanes: 4, laneRate: "25.78125 Gbaud", modulation: "NRZ", fiberType: "MMF (OM3/OM4)", wavelength: "850nm", maxReachMeters: 100, maxReachLabel: "70m (OM3) / 100m (OM4)", connector: "MPO-12", fecRequired: true, formFactors: ["QSFP28"], yearRatified: 2015, notes: "4x25G parallel. Breakout to 4x25GBASE-SR." }, + { standard: "100GBASE-SR2", ieeeReference: "100G Lambda MSA", speed: "100G", lanes: 2, laneRate: "26.5625 Gbaud", modulation: "PAM4", fiberType: "MMF (OM4)", wavelength: "850nm", maxReachMeters: 100, maxReachLabel: "100m (OM4)", connector: "LC", fecRequired: true, formFactors: ["QSFP28"], yearRatified: 2018, notes: "MSA-defined. 2x50G PAM4 over duplex LC." }, + { standard: "100GBASE-DR", ieeeReference: "IEEE 802.3cd", speed: "100G", lanes: 1, laneRate: "53.125 Gbaud", modulation: "PAM4", fiberType: "SMF", wavelength: "1310nm", maxReachMeters: 500, maxReachLabel: "500m", connector: "LC", fecRequired: true, formFactors: ["QSFP28", "SFP-DD"], yearRatified: 2018, notes: "Single-lambda 100G. Key for leaf-spine architectures." }, + { standard: "100GBASE-FR1", ieeeReference: "IEEE 802.3cu", speed: "100G", lanes: 1, laneRate: "53.125 Gbaud", modulation: "PAM4", fiberType: "SMF", wavelength: "1310nm", maxReachMeters: 2000, maxReachLabel: "2km", connector: "LC", fecRequired: true, formFactors: ["QSFP28"], yearRatified: 2021, notes: "Single-lambda 100G for 2km." }, + { standard: "100GBASE-LR1", ieeeReference: "IEEE 802.3cu", speed: "100G", lanes: 1, laneRate: "53.125 Gbaud", modulation: "PAM4", fiberType: "SMF", wavelength: "1310nm", maxReachMeters: 10000, maxReachLabel: "10km", connector: "LC", fecRequired: true, formFactors: ["QSFP28"], yearRatified: 2021, notes: "Single-lambda 100G for 10km. New IXP standard replacing LR4." }, + { standard: "100GBASE-LR4", ieeeReference: "IEEE 802.3ba", speed: "100G", lanes: 4, laneRate: "25.78125 Gbaud", modulation: "NRZ", fiberType: "SMF", wavelength: "1310nm (4 LAN-WDM wavelengths)", maxReachMeters: 10000, maxReachLabel: "10km", connector: "LC", fecRequired: false, formFactors: ["QSFP28", "CFP", "CFP2", "CFP4"], yearRatified: 2010, notes: "4x25G LAN-WDM over duplex LC. Being replaced by LR1 single-lambda." }, + { standard: "100GBASE-CWDM4", ieeeReference: "100G CWDM4 MSA", speed: "100G", lanes: 4, laneRate: "25.78125 Gbaud", modulation: "NRZ", fiberType: "SMF", wavelength: "1271/1291/1311/1331nm", maxReachMeters: 2000, maxReachLabel: "2km", connector: "LC", fecRequired: false, formFactors: ["QSFP28"], yearRatified: 2014, notes: "MSA-defined. Lower-cost alternative to LR4 for 2km." }, + { standard: "100GBASE-PSM4", ieeeReference: "100G PSM4 MSA", speed: "100G", lanes: 4, laneRate: "25.78125 Gbaud", modulation: "NRZ", fiberType: "SMF (parallel)", wavelength: "1310nm", maxReachMeters: 500, maxReachLabel: "500m", connector: "MPO-12", fecRequired: false, formFactors: ["QSFP28"], yearRatified: 2014, notes: "MSA-defined. 4x25G parallel single-mode." }, + { standard: "100GBASE-ZR", ieeeReference: "IEEE 802.3ct", speed: "100G", lanes: 1, laneRate: "~64 Gbaud", modulation: "DP-QPSK (coherent)", fiberType: "SMF (DWDM)", wavelength: "C-band (tunable)", maxReachMeters: 80000, maxReachLabel: "80km+ (DWDM amplified)", connector: "LC", fecRequired: true, formFactors: ["QSFP28"], yearRatified: 2021, notes: "Coherent 100G over DWDM systems." }, + + // 200G + { standard: "200GBASE-SR4", ieeeReference: "IEEE 802.3cd", speed: "200G", lanes: 4, laneRate: "26.5625 Gbaud", modulation: "PAM4", fiberType: "MMF (OM4)", wavelength: "850nm", maxReachMeters: 100, maxReachLabel: "70m (OM3) / 100m (OM4)", connector: "MPO-12", fecRequired: true, formFactors: ["QSFP56", "QSFP-DD"], yearRatified: 2018, notes: "4x50G PAM4 parallel." }, + { standard: "200GBASE-DR4", ieeeReference: "IEEE 802.3cd", speed: "200G", lanes: 4, laneRate: "26.5625 Gbaud", modulation: "PAM4", fiberType: "SMF (parallel)", wavelength: "1310nm", maxReachMeters: 500, maxReachLabel: "500m", connector: "MPO-12", fecRequired: true, formFactors: ["QSFP56", "QSFP-DD"], yearRatified: 2018, notes: "4x50G parallel SMF. Can break out to 2x100G-DR or 4x50G." }, + { standard: "200GBASE-FR4", ieeeReference: "IEEE 802.3cu", speed: "200G", lanes: 4, laneRate: "26.5625 Gbaud", modulation: "PAM4", fiberType: "SMF", wavelength: "1310nm (4 CWDM wavelengths)", maxReachMeters: 2000, maxReachLabel: "2km", connector: "LC", fecRequired: true, formFactors: ["QSFP56", "QSFP-DD"], yearRatified: 2021, notes: "4x50G CWDM over duplex LC for 2km reach." }, + { standard: "200GBASE-LR4", ieeeReference: "IEEE 802.3cu", speed: "200G", lanes: 4, laneRate: "26.5625 Gbaud", modulation: "PAM4", fiberType: "SMF", wavelength: "1310nm (4 CWDM wavelengths)", maxReachMeters: 10000, maxReachLabel: "10km", connector: "LC", fecRequired: true, formFactors: ["QSFP56", "QSFP-DD"], yearRatified: 2021, notes: "4x50G CWDM over duplex LC for 10km reach." }, + + // 400G + { standard: "400GBASE-SR8", ieeeReference: "IEEE 802.3cm", speed: "400G", lanes: 8, laneRate: "26.5625 Gbaud", modulation: "PAM4", fiberType: "MMF (OM4)", wavelength: "850nm", maxReachMeters: 100, maxReachLabel: "100m (OM4)", connector: "MPO-16", fecRequired: true, formFactors: ["QSFP-DD", "OSFP"], yearRatified: 2020, notes: "8x50G PAM4 parallel." }, + { standard: "400GBASE-SR4.2", ieeeReference: "IEEE 802.3cm", speed: "400G", lanes: 4, laneRate: "26.5625 Gbaud", modulation: "PAM4", fiberType: "MMF", wavelength: "850nm + 910nm (BiDi)", maxReachMeters: 100, maxReachLabel: "100m", connector: "MPO-12", fecRequired: true, formFactors: ["QSFP-DD", "OSFP"], yearRatified: 2020, notes: "BiDi 400G over MPO-12 using two wavelengths." }, + { standard: "400GBASE-DR4", ieeeReference: "IEEE 802.3bs", speed: "400G", lanes: 4, laneRate: "53.125 Gbaud", modulation: "PAM4", fiberType: "SMF (parallel)", wavelength: "1310nm", maxReachMeters: 500, maxReachLabel: "500m", connector: "MPO-12", fecRequired: true, formFactors: ["QSFP-DD", "OSFP"], yearRatified: 2017, notes: "4x100G parallel SMF. THE key 400G data center optic." }, + { standard: "400GBASE-FR4", ieeeReference: "IEEE 802.3cu", speed: "400G", lanes: 4, laneRate: "53.125 Gbaud", modulation: "PAM4", fiberType: "SMF", wavelength: "1271/1291/1311/1331nm (CWDM4)", maxReachMeters: 2000, maxReachLabel: "2km", connector: "LC", fecRequired: true, formFactors: ["QSFP-DD", "OSFP"], yearRatified: 2021, notes: "4x100G CWDM over duplex LC." }, + { standard: "400GBASE-LR4-10", ieeeReference: "IEEE 802.3cu", speed: "400G", lanes: 4, laneRate: "53.125 Gbaud", modulation: "PAM4", fiberType: "SMF", wavelength: "1271/1291/1311/1331nm (CWDM4)", maxReachMeters: 10000, maxReachLabel: "10km", connector: "LC", fecRequired: true, formFactors: ["QSFP-DD", "OSFP"], yearRatified: 2021, notes: "4x100G CWDM for 10km. Standard for metro/IXP 400G." }, + { standard: "400GBASE-ZR (OIF 400ZR)", ieeeReference: "OIF-400ZR-01.0", speed: "400G", lanes: 1, laneRate: "~60 Gbaud", modulation: "DP-16QAM (coherent)", fiberType: "SMF", wavelength: "C-band (tunable, 75 GHz DWDM grid)", maxReachMeters: 120000, maxReachLabel: "up to 120km (amplified)", connector: "LC", fecRequired: true, formFactors: ["QSFP-DD", "OSFP", "CFP2-DCO"], yearRatified: 2020, notes: "OIF interoperable coherent 400G. Collapses IP/optical layers." }, + + // 800G + { standard: "800GBASE-SR8", ieeeReference: "IEEE 802.3df", speed: "800G", lanes: 8, laneRate: "106.25 Gbaud", modulation: "PAM4", fiberType: "MMF (OM4)", wavelength: "850nm", maxReachMeters: 50, maxReachLabel: "50m (OM3) / 100m (OM4)", connector: "2x MPO-12", fecRequired: true, formFactors: ["OSFP"], yearRatified: 2024, notes: "8x100G PAM4 parallel." }, + { standard: "800GBASE-DR8", ieeeReference: "IEEE 802.3df", speed: "800G", lanes: 8, laneRate: "106.25 Gbaud", modulation: "PAM4", fiberType: "SMF (parallel)", wavelength: "1310nm", maxReachMeters: 500, maxReachLabel: "500m", connector: "2x MPO-12", fecRequired: true, formFactors: ["OSFP", "QSFP-DD800"], yearRatified: 2024, notes: "Primary 800G DC optic. 8x100G parallel SMF." }, + { standard: "OIF 800ZR", ieeeReference: "OIF-800ZR", speed: "800G", lanes: 1, laneRate: "~90 Gbaud", modulation: "DP-16QAM / DP-64QAM (coherent)", fiberType: "SMF", wavelength: "C-band (tunable)", maxReachMeters: 120000, maxReachLabel: "up to 120km+ (amplified)", connector: "LC", fecRequired: true, formFactors: ["OSFP", "QSFP-DD800", "CFP2-DCO"], yearRatified: 2024, notes: "800G pluggable coherent. Building on 400ZR success for DCI." }, +]; + +/** Find a standard by exact or partial name. */ +export function getStandard(name: string): Standard | undefined { + const q = name.toLowerCase(); + return standards.find((s) => s.standard.toLowerCase() === q) || + standards.find((s) => s.standard.toLowerCase().includes(q)); +} + +/** Search standards by keyword (speed, modulation, IEEE reference, etc.). */ +export function searchStandards(query: string): Standard[] { + const q = query.toLowerCase(); + return standards.filter( + (s) => + s.standard.toLowerCase().includes(q) || + s.speed.toLowerCase().includes(q) || + s.ieeeReference.toLowerCase().includes(q) || + s.modulation.toLowerCase().includes(q) || + s.notes.toLowerCase().includes(q) + ); +} diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts new file mode 100644 index 0000000..451022e --- /dev/null +++ b/packages/core/src/types.ts @@ -0,0 +1,137 @@ +/** + * Core type definitions for the transceiver database. + */ + +export type FormFactor = + | "SFP" + | "SFP+" + | "SFP28" + | "SFP56" + | "QSFP+" + | "QSFP28" + | "QSFP56" + | "QSFP-DD" + | "OSFP" + | "CFP" + | "CFP2" + | "CFP4" + | "CFP2-DCO" + | "XFP" + | "GBIC" + | "CXP" + | "SFP-DD" + | "SFP56-DD" + | "QSFP-DD800" + | "OSFP-XD"; + +export type FiberType = "MMF" | "SMF" | "MMF/SMF" | "Copper" | "N/A"; + +export type ConnectorType = + | "LC" + | "SC" + | "MPO-12" + | "MPO-16" + | "MPO-24" + | "RJ45" + | "None" + | "CS" + | "SN" + | "2xMPO-12"; + +export type TempRange = "COM" | "IND"; + +export type ProductCategory = + | "DataCenter" + | "Metro" + | "LongHaul" + | "DCI" + | "Access" + | "Coherent" + | "CWDM" + | "DWDM" + | "BiDi" + | "AOC" + | "DAC" + | "Breakout" + | "Legacy" + | "IXP" + | "5G" + | "AI"; + +export type PriceTier = "Budget" | "Standard" | "Premium"; + +export type MarketStatus = "Mainstream" | "Growth" | "Emerging" | "Legacy" | "EOL"; + +export interface VendorCompat { + vendor: string; + partPattern: string; +} + +export interface Transceiver { + id: string; + standard: string; + ieeeReference?: string; + formFactor: FormFactor; + speed: string; + speedGbps: number; + lanes?: number; + laneRate?: string; + modulation?: string; + reachMeters: number; + reachLabel: string; + fiberType: FiberType; + wavelengths: string; + connector: ConnectorType; + powerConsumptionW: number; + tempRange: TempRange; + category: ProductCategory; + priceTier: PriceTier; + useCase: string; + vendors: VendorCompat[]; + tags: string[]; + generation?: string; + marketStatus?: MarketStatus; + yearIntroduced?: number; + breakoutCapable?: boolean; + breakoutTo?: string; +} + +export interface Standard { + standard: string; + ieeeReference: string; + speed: string; + lanes: number; + laneRate: string; + modulation: string; + fiberType: string; + wavelength: string; + maxReachMeters: number; + maxReachLabel: string; + connector: string; + fecRequired: boolean; + formFactors: string[]; + yearRatified: number; + notes: string; +} + +export interface Competitor { + name: string; + type: "OEM" | "Whitebox" | "Manufacturer" | "Distributor" | "Compatible"; + headquarters: string; + marketPosition: string; + formFactorsOffered: string[]; + speedTiersOffered: string[]; + strengths: string[]; + weaknesses: string[]; +} + +export interface Breakout { + id: string; + from: string; + to: string; + formFactor: string; + description: string; + cableType: "Passive" | "Active"; + maxLength: string; + speedPerLane: string; +} diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 0000000..b2640bd --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "moduleResolution": "node" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/scraper/package.json b/packages/scraper/package.json new file mode 100644 index 0000000..883f9d2 --- /dev/null +++ b/packages/scraper/package.json @@ -0,0 +1,31 @@ +{ + "name": "@tip/scraper", + "version": "0.1.0", + "private": true, + "description": "TIP scraper engine — Crawlee + Playwright for competitor pricing, stock, datasheets, FAQs", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "dev": "tsx src/index.ts", + "scrape:fs": "tsx src/scrapers/fs-com.ts", + "scrape:cisco": "tsx src/scrapers/cisco-tmg.ts", + "scrape:optcore": "tsx src/scrapers/optcore.ts", + "scrape:news": "tsx src/scrapers/news.ts", + "scrape:all": "tsx src/index.ts --all" + }, + "dependencies": { + "crawlee": "^3.12.0", + "playwright": "^1.50.0", + "pg": "^8.13.1", + "pg-boss": "^10.1.5", + "dotenv": "^16.4.7", + "cheerio": "^1.0.0", + "xml2js": "^0.6.2" + }, + "devDependencies": { + "@types/pg": "^8.11.11", + "@types/xml2js": "^0.4.14", + "typescript": "^5.9.3", + "tsx": "^4.19.0" + } +} diff --git a/packages/scraper/src/index.ts b/packages/scraper/src/index.ts new file mode 100644 index 0000000..ce94f6d --- /dev/null +++ b/packages/scraper/src/index.ts @@ -0,0 +1,69 @@ +/** + * TIP Scraper Engine — Main entry point. + * + * Usage: + * tsx src/index.ts — Start scheduler (production mode) + * tsx src/index.ts --all — Run all scrapers once + * tsx src/index.ts --fs — Run FS.com scraper once + * tsx src/index.ts --cisco — Run Cisco TMG scraper once + * tsx src/index.ts --optcore — Run Optcore scraper once + * tsx src/index.ts --news — Run news aggregator once + */ +import { createScheduler, registerSchedules, registerWorkers } from "./scheduler"; +import { scrapeFs } from "./scrapers/fs-com"; +import { scrapeCiscoTmg } from "./scrapers/cisco-tmg"; +import { scrapeOptcore } from "./scrapers/optcore"; +import { scrapeNews } from "./scrapers/news"; +import { pool } from "./utils/db"; + +const args = process.argv.slice(2); + +async function runOnce(): Promise { + if (args.includes("--fs") || args.includes("--all")) { + await scrapeFs(); + } + if (args.includes("--cisco") || args.includes("--all")) { + await scrapeCiscoTmg(); + } + if (args.includes("--optcore") || args.includes("--all")) { + await scrapeOptcore(); + } + if (args.includes("--news") || args.includes("--all")) { + await scrapeNews(); + } + await pool.end(); +} + +async function runScheduler(): Promise { + console.log("=== TIP Scraper Engine ===\n"); + console.log("Mode: Scheduler (pg-boss)\n"); + + const boss = await createScheduler(); + await registerSchedules(boss); + await registerWorkers(boss); + + console.log("\nScheduler running. Press Ctrl+C to stop.\n"); + + // Graceful shutdown + const shutdown = async () => { + console.log("\nShutting down..."); + await boss.stop(); + await pool.end(); + process.exit(0); + }; + + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); +} + +if (args.some((a) => ["--all", "--fs", "--cisco", "--optcore", "--news"].includes(a))) { + runOnce().catch((err) => { + console.error("Fatal:", err); + process.exit(1); + }); +} else { + runScheduler().catch((err) => { + console.error("Fatal:", err); + process.exit(1); + }); +} diff --git a/packages/scraper/src/scheduler.ts b/packages/scraper/src/scheduler.ts new file mode 100644 index 0000000..b117616 --- /dev/null +++ b/packages/scraper/src/scheduler.ts @@ -0,0 +1,127 @@ +/** + * pg-boss Job Scheduler — manages scrape jobs with adaptive timing. + * + * Job types: + * scrape:pricing:fs — Every 4 hours for FS.com prices/stock + * scrape:pricing:optcore — Every 6 hours for Optcore prices/stock + * scrape:compat:cisco — Weekly for OEM compatibility matrices + * scrape:news — Every 6 hours for trade press and news + * scrape:docs — Weekly for manuals and datasheets + * scrape:faq — Weekly for vendor FAQ/troubleshooting pages + */ +import PgBoss from "pg-boss"; +import { config } from "dotenv"; +import { join } from "path"; + +config({ path: join(__dirname, "..", "..", "..", ".env") }); + +const connectionString = `postgres://${process.env.POSTGRES_USER || "tip"}:${process.env.POSTGRES_PASSWORD || "tip_dev_2026"}@${process.env.POSTGRES_HOST || "localhost"}:${process.env.POSTGRES_PORT || "5433"}/${process.env.POSTGRES_DB || "transceiver_db"}`; + +export async function createScheduler(): Promise { + const boss = new PgBoss({ + connectionString, + retryLimit: 3, + retryDelay: 30, + retryBackoff: true, + expireInSeconds: 300, // 5 min timeout per job + monitorStateIntervalSeconds: 30, + }); + + boss.on("error", (error) => console.error("pg-boss error:", error)); + + await boss.start(); + console.log("pg-boss scheduler started"); + + return boss; +} + +export async function registerSchedules(boss: PgBoss): Promise { + // pg-boss v10: create queues before scheduling + const queues = [ + "scrape:pricing:fs", + "scrape:pricing:optcore", + "scrape:compat:cisco", + "scrape:news", + "scrape:faq", + "scrape:docs", + ]; + for (const q of queues) { + await boss.createQueue(q).catch(() => { /* already exists */ }); + } + + // FS.com pricing (every 4 hours — JS rendering is slow) + await boss.schedule("scrape:pricing:fs", "0 */4 * * *", {}, { + retryLimit: 2, + expireInSeconds: 3600, + }); + + // Optcore pricing (every 6 hours — WP API enumeration + Playwright) + await boss.schedule("scrape:pricing:optcore", "0 */6 * * *", {}, { + retryLimit: 2, + expireInSeconds: 7200, + }); + + // Compatibility matrices (every Sunday at 3am) + await boss.schedule("scrape:compat:cisco", "0 3 * * 0", {}, { + retryLimit: 3, + expireInSeconds: 3600, + }); + + // News aggregation (every 6 hours) + await boss.schedule("scrape:news", "0 */6 * * *", {}, { + retryLimit: 2, + expireInSeconds: 1800, + }); + + // FAQ/KB scraping (every Wednesday at 2am) + await boss.schedule("scrape:faq", "0 2 * * 3", {}, { + retryLimit: 3, + expireInSeconds: 3600, + }); + + // Document/datasheet check (every Saturday at 4am) + await boss.schedule("scrape:docs", "0 4 * * 6", {}, { + retryLimit: 3, + expireInSeconds: 7200, + }); + + console.log("All schedules registered"); +} + +export async function registerWorkers(boss: PgBoss): Promise { + // Lazy-load scrapers to avoid circular deps + const { scrapeFs } = await import("./scrapers/fs-com"); + const { scrapeCiscoTmg } = await import("./scrapers/cisco-tmg"); + const { scrapeOptcore } = await import("./scrapers/optcore"); + const { scrapeNews } = await import("./scrapers/news"); + + await boss.work("scrape:pricing:fs", async (_job) => { + console.log(`[${new Date().toISOString()}] Running: FS.com pricing`); + await scrapeFs(); + }); + + await boss.work("scrape:pricing:optcore", async (_job) => { + console.log(`[${new Date().toISOString()}] Running: Optcore pricing`); + await scrapeOptcore(); + }); + + await boss.work("scrape:compat:cisco", async (_job) => { + console.log(`[${new Date().toISOString()}] Running: Cisco TMG`); + await scrapeCiscoTmg(); + }); + + await boss.work("scrape:news", async (_job) => { + console.log(`[${new Date().toISOString()}] Running: News aggregation`); + await scrapeNews(); + }); + + await boss.work("scrape:faq", async (_job) => { + console.log(`[${new Date().toISOString()}] FAQ scraper — not yet implemented`); + }); + + await boss.work("scrape:docs", async (_job) => { + console.log(`[${new Date().toISOString()}] Docs scraper — not yet implemented`); + }); + + console.log("All workers registered"); +} diff --git a/packages/scraper/src/scrapers/cisco-tmg.ts b/packages/scraper/src/scrapers/cisco-tmg.ts new file mode 100644 index 0000000..79cb6fd --- /dev/null +++ b/packages/scraper/src/scrapers/cisco-tmg.ts @@ -0,0 +1,155 @@ +/** + * Cisco TMG Matrix Scraper — Transceiver Compatibility + * + * Source: tmgmatrix.cisco.com + * Extracts: Switch model ↔ Transceiver compatibility data + * Stores: switches, compatibility table + * + * The TMG Matrix has a JSON API behind the scenes. + */ +import { CheerioCrawler } from "crawlee"; +import { pool, ensureVendor } from "../utils/db"; + +const TMG_BASE = "https://tmgmatrix.cisco.com"; + +interface TmgEntry { + switchModel: string; + switchSeries: string; + transceiverPid: string; + transceiverDescription: string; + speed: string; + reach: string; + cableType: string; + connector: string; + minSoftware: string; +} + +async function upsertCiscoSwitch(vendorId: string, model: string, series: string): Promise { + const result = await pool.query( + `INSERT INTO switches (vendor_id, model, series, category, layer, managed) + VALUES ($1, $2, $3, 'DataCenter', 'L3', true) + ON CONFLICT (vendor_id, model) DO UPDATE SET series = EXCLUDED.series + RETURNING id`, + [vendorId, model, series] + ); + return result.rows[0].id; +} + +async function upsertCompatibility( + switchId: string, + transceiverId: string, + firmwareMin: string +): Promise { + await pool.query( + `INSERT INTO compatibility (switch_id, transceiver_id, verified_by, verification_method, status, firmware_min, source_url) + VALUES ($1, $2, 'Cisco TMG Matrix', 'vendor_matrix', 'compatible', $3, $4) + ON CONFLICT (switch_id, transceiver_id) DO UPDATE SET firmware_min = EXCLUDED.firmware_min`, + [switchId, transceiverId, firmwareMin || null, TMG_BASE] + ); +} + +export async function scrapeCiscoTmg(): Promise { + console.log("=== Cisco TMG Matrix Scraper Starting ===\n"); + + const ciscoVendorId = await ensureVendor( + "Cisco", + "oem", + "https://www.cisco.com", + undefined + ); + + const entries: TmgEntry[] = []; + + // TMG Matrix uses a search API + // First, try the public HTML interface + const crawler = new CheerioCrawler({ + maxConcurrency: 1, + maxRequestsPerMinute: 10, // Very respectful — Cisco rate limits aggressively + + async requestHandler({ request, $, log }) { + log.info(`Scraping: ${request.url}`); + + // The TMG Matrix renders a table with compatibility data + $("table tbody tr, .matrix-row, [class*='result-row']").each((_i, el) => { + const $row = $(el); + const cells = $row.find("td").map((_j, td) => $(td).text().trim()).get(); + + if (cells.length >= 4) { + entries.push({ + switchModel: cells[0] || "", + switchSeries: cells[0]?.split(" ")[0] || "Nexus", + transceiverPid: cells[1] || "", + transceiverDescription: cells[2] || "", + speed: cells[3] || "", + reach: cells[4] || "", + cableType: cells[5] || "", + connector: cells[6] || "", + minSoftware: cells[7] || "", + }); + } + }); + }, + }); + + // Start with Nexus switches (most relevant for Flexoptix) + await crawler.run([ + `${TMG_BASE}/public/tmg?searchValue=Nexus+9000`, + `${TMG_BASE}/public/tmg?searchValue=Nexus+3000`, + `${TMG_BASE}/public/tmg?searchValue=Nexus+7000`, + `${TMG_BASE}/public/tmg?searchValue=Catalyst+9000`, + ]); + + console.log(`\nEntries found: ${entries.length}`); + + // Write to database + let switches = 0; + let compat = 0; + + for (const entry of entries) { + if (!entry.switchModel || !entry.transceiverPid) continue; + + try { + const switchId = await upsertCiscoSwitch( + ciscoVendorId, + entry.switchModel, + entry.switchSeries + ); + switches++; + + // Try to match transceiver in our DB + const txResult = await pool.query( + `SELECT id FROM transceivers + WHERE part_number = $1 + OR slug LIKE $2 + OR standard_name ILIKE $3 + LIMIT 1`, + [ + entry.transceiverPid, + `%${entry.transceiverPid.toLowerCase().replace(/[^a-z0-9]/g, "")}%`, + `%${entry.speed}%${entry.reach}%`, + ] + ); + + if (txResult.rows.length > 0) { + await upsertCompatibility(switchId, txResult.rows[0].id, entry.minSoftware); + compat++; + } + } catch (err) { + // Skip duplicates silently + } + } + + console.log(`Switches upserted: ${switches}`); + console.log(`Compatibility entries: ${compat}`); + console.log("=== Cisco TMG Scraper Complete ===\n"); +} + +if (require.main === module) { + scrapeCiscoTmg() + .then(() => pool.end()) + .catch((err) => { + console.error("Fatal:", err); + pool.end(); + process.exit(1); + }); +} diff --git a/packages/scraper/src/scrapers/fs-com.ts b/packages/scraper/src/scrapers/fs-com.ts new file mode 100644 index 0000000..1bcf84f --- /dev/null +++ b/packages/scraper/src/scrapers/fs-com.ts @@ -0,0 +1,277 @@ +/** + * FS.com Scraper — Prices, Stock, Product Catalog + * + * FS.com renders products client-side (JS), so we use PlaywrightCrawler. + * Categories: /c/optical-transceivers-9 + * + * Respects: robots.txt, rate limiting (2s between requests) + */ +import { PlaywrightCrawler } from "crawlee"; +import { ensureVendor, upsertPriceObservation, findOrCreateScrapedTransceiver, pool } from "../utils/db"; +import { contentHash, parsePrice, parseStockLevel, parseQuantity } from "../utils/hash"; + +const BASE_URL = "https://www.fs.com"; + +const CATEGORY_URLS = [ + "/c/1g-sfp-modules-702", + "/c/10g-sfp-plus-modules-703", + "/c/25g-sfp28-modules-704", + "/c/40g-qsfp-plus-modules-705", + "/c/100g-qsfp28-modules-706", + "/c/400g-qsfp-dd-modules-3102", + "/c/800g-osfp-modules-3449", +]; + +interface FsProduct { + partNumber: string; + name: string; + price: number; + currency: string; + stockLevel: string; + quantity?: number; + url: string; + formFactor?: string; + speedGbps?: number; + speed?: string; + reachLabel?: string; +} + +function detectFormFactor(text: string): string | undefined { + const lower = text.toLowerCase(); + if (lower.includes("osfp") && !lower.includes("qsfp")) return "OSFP"; + if (lower.includes("qsfp-dd800") || lower.includes("qsfp-dd 800")) return "QSFP-DD800"; + if (lower.includes("qsfp-dd")) return "QSFP-DD"; + if (lower.includes("qsfp56")) return "QSFP56"; + if (lower.includes("qsfp28")) return "QSFP28"; + if (lower.includes("qsfp+") || lower.includes("qsfp plus")) return "QSFP+"; + if (lower.includes("sfp56")) return "SFP56"; + if (lower.includes("sfp28")) return "SFP28"; + if (lower.includes("sfp+") || lower.includes("sfp plus")) return "SFP+"; + if (lower.includes("sfp") && !lower.includes("qsfp")) return "SFP"; + if (lower.includes("cfp2")) return "CFP2"; + if (lower.includes("xfp")) return "XFP"; + return undefined; +} + +function detectSpeed(text: string): { speed: string; speedGbps: number } | undefined { + const patterns: [RegExp, string, number][] = [ + [/800\s*g/i, "800G", 800], + [/400\s*g/i, "400G", 400], + [/200\s*g/i, "200G", 200], + [/100\s*g/i, "100G", 100], + [/50\s*g/i, "50G", 50], + [/40\s*g/i, "40G", 40], + [/25\s*g/i, "25G", 25], + [/10\s*g/i, "10G", 10], + [/1\s*g\b/i, "1G", 1], + ]; + for (const [re, speed, gbps] of patterns) { + if (re.test(text)) return { speed, speedGbps: gbps }; + } + return undefined; +} + +function detectReach(text: string): string | undefined { + const match = text.match(/(\d+)\s*(m|km)\b/i); + if (match) return `${match[1]}${match[2].toLowerCase()}`; + return undefined; +} + +export async function scrapeFs(): Promise { + console.log("=== FS.com Scraper Starting ===\n"); + + const vendorId = await ensureVendor( + "FS.COM", + "compatible", + "https://www.fs.com", + "https://www.fs.com/c/optical-transceivers-9" + ); + console.log(`Vendor ID: ${vendorId}`); + + const products: FsProduct[] = []; + let pagesScraped = 0; + + const crawler = new PlaywrightCrawler({ + maxConcurrency: 1, + maxRequestsPerMinute: 15, + requestHandlerTimeoutSecs: 60, + headless: true, + launchContext: { + launchOptions: { + args: ["--disable-blink-features=AutomationControlled"], + }, + }, + + async requestHandler({ page, request, log }) { + const url = request.url; + log.info(`Scraping: ${url}`); + + // Wait for product list to render + await page.waitForTimeout(3000); + + // Try multiple selectors — FS.com changes DOM frequently + const productData = await page.evaluate(() => { + const results: Array<{ + name: string; + href: string; + price: string; + stock: string; + partNumber: string; + }> = []; + + // Strategy 1: Look for product links with prices nearby + const productLinks = document.querySelectorAll( + 'a[href*="/products/"], a[href*="/product/"], .product-item a, .o-list-product a, [class*="product"] a[href]' + ); + + for (const link of productLinks) { + const el = link as HTMLAnchorElement; + const name = el.textContent?.trim() || ""; + const href = el.getAttribute("href") || ""; + + if (!name || name.length < 5 || !href) continue; + + // Find price in parent/sibling elements + const container = + el.closest('[class*="product"]') || + el.closest('[class*="item"]') || + el.closest("li") || + el.parentElement?.parentElement; + + let price = ""; + let stock = ""; + + if (container) { + const priceEl = container.querySelector( + '[class*="price"], [class*="Price"], .o-price, span[data-price]' + ); + price = priceEl?.textContent?.trim() || ""; + + const stockEl = container.querySelector( + '[class*="stock"], [class*="Stock"], [class*="avail"], .o-stock' + ); + stock = stockEl?.textContent?.trim() || ""; + } + + // Extract part number from URL or text + const pn = href.split("/").pop()?.replace(".html", "")?.replace("#", "") || ""; + + if (name && (price || href.includes("/product"))) { + results.push({ name, href, price, stock, partNumber: pn }); + } + } + + // Strategy 2: Look for any element with $ or US$ price pattern + if (results.length === 0) { + const allText = document.querySelectorAll("*"); + for (const el of allText) { + const text = el.textContent || ""; + if (/US?\$\s*\d+\.\d{2}/.test(text) && text.length < 200) { + const linkEl = el.closest("a") || el.querySelector("a"); + if (linkEl) { + results.push({ + name: linkEl.textContent?.trim() || text.slice(0, 100), + href: linkEl.getAttribute("href") || "", + price: text.match(/US?\$\s*[\d,.]+/)?.[0] || "", + stock: "", + partNumber: "", + }); + } + } + } + } + + return results; + }); + + for (const item of productData) { + if (!item.name || !item.price) continue; + + const { price, currency } = parsePrice(item.price); + const speedInfo = detectSpeed(item.name); + + if (price > 0) { + products.push({ + partNumber: item.partNumber || item.name.slice(0, 50), + name: item.name, + price, + currency, + stockLevel: item.stock ? parseStockLevel(item.stock) : "on_request", + quantity: item.stock ? parseQuantity(item.stock) : undefined, + url: item.href.startsWith("http") ? item.href : `${BASE_URL}${item.href}`, + formFactor: detectFormFactor(item.name), + speedGbps: speedInfo?.speedGbps, + speed: speedInfo?.speed, + reachLabel: detectReach(item.name), + }); + } + } + + pagesScraped++; + log.info(` Found ${productData.length} items on page`); + }, + }); + + const startUrls = CATEGORY_URLS.map((path) => `${BASE_URL}${path}`); + await crawler.run(startUrls); + + console.log(`\nPages scraped: ${pagesScraped}`); + console.log(`Products found: ${products.length}`); + + // Deduplicate by partNumber + const uniqueProducts = new Map(); + for (const p of products) { + const key = p.partNumber || p.name; + if (!uniqueProducts.has(key)) { + uniqueProducts.set(key, p); + } + } + + // Write to database + let written = 0; + let skipped = 0; + + for (const p of uniqueProducts.values()) { + try { + const transceiverId = await findOrCreateScrapedTransceiver({ + partNumber: p.partNumber, + vendorId, + formFactor: p.formFactor, + speedGbps: p.speedGbps, + speed: p.speed, + reachLabel: p.reachLabel, + category: "DataCenter", + }); + + const hash = contentHash({ price: p.price, stock: p.stockLevel, qty: p.quantity }); + const isNew = await upsertPriceObservation({ + transceiverId, + sourceVendorId: vendorId, + price: p.price, + currency: p.currency, + stockLevel: p.stockLevel, + quantityAvailable: p.quantity, + url: p.url, + contentHash: hash, + }); + + if (isNew) written++; + else skipped++; + } catch (err) { + console.error(` Error: ${p.partNumber}:`, (err as Error).message); + } + } + + console.log(`\nDatabase: ${written} new, ${skipped} unchanged (${uniqueProducts.size} unique)`); + console.log("=== FS.com Scraper Complete ===\n"); +} + +if (require.main === module) { + scrapeFs() + .then(() => pool.end()) + .catch((err) => { + console.error("Fatal:", err); + pool.end(); + process.exit(1); + }); +} diff --git a/packages/scraper/src/scrapers/news.ts b/packages/scraper/src/scrapers/news.ts new file mode 100644 index 0000000..b05c71f --- /dev/null +++ b/packages/scraper/src/scrapers/news.ts @@ -0,0 +1,269 @@ +/** + * News Aggregator — Optics & Fiber Trade Press RSS Scraper + * + * Sources: + * - optics.org (photonics industry news) + * - SPIE Newsroom (photonics research) + * - Network World (data center / networking) + * - Light Reading (telecom) + * - Telecom Ramblings (industry commentary) + * + * Stores articles in news_articles table. + * Relevance filtering: keyword scoring for transceiver/optics topics. + */ +import { pool } from "../utils/db"; +import { contentHash } from "../utils/hash"; +import { parseStringPromise } from "xml2js"; + +// Categories allowed by news_articles CHECK constraint +type NewsCategory = "product_launch" | "market_report" | "standard" | "m_and_a" | "factory" | "event"; + +interface RssFeed { + name: string; + url: string; + category: NewsCategory; +} + +interface NewsArticle { + title: string; + sourceUrl: string; + summary: string; + publishedAt: Date; + source: string; + category: NewsCategory | null; + relevanceScore: number; + contentHash: string; +} + +const FEEDS: RssFeed[] = [ + { + name: "Optics.org", + url: "https://optics.org/rss/news", + category: "market_report", + }, + { + name: "SPIE Newsroom", + url: "https://www.spie.org/newsroom/rss.xml", + category: "market_report", + }, + { + name: "Network World - Data Center", + url: "https://www.networkworld.com/category/data-center/index.rss", + category: "market_report", + }, + { + name: "CableFree", + url: "https://www.cablefree.net/rss", + category: "market_report", + }, + { + name: "Nature Photonics", + url: "https://www.nature.com/nphoton.rss", + category: "standard", + }, +]; + +// Keywords for relevance scoring +const HIGH_RELEVANCE = [ + "transceiver", "sfp", "qsfp", "xfp", "cfp", "osfp", + "optical module", "fiber optic", "wavelength", "dwdm", "cwdm", + "400g", "800g", "1.6t", "coherent", "pluggable", + "ofc", "ecoc", "cioe", +]; + +const MEDIUM_RELEVANCE = [ + "data center", "datacenter", "interconnect", "bandwidth", + "switch", "router", "cisco", "arista", "juniper", + "100g", "40g", "25g", "10g", + "silicon photonics", "photonic", + "ii-vi", "coherent", "lumentum", "inphi", + "flexoptix", "prolabs", +]; + +function scoreRelevance(title: string, summary: string): number { + const text = `${title} ${summary}`.toLowerCase(); + let score = 0; + + for (const kw of HIGH_RELEVANCE) { + if (text.includes(kw)) score += 3; + } + for (const kw of MEDIUM_RELEVANCE) { + if (text.includes(kw)) score += 1; + } + + return score; +} + +async function fetchFeed(feed: RssFeed): Promise { + const articles: NewsArticle[] = []; + + try { + const resp = await fetch(feed.url, { + headers: { + "User-Agent": "Mozilla/5.0 (compatible; TIP-NewsBot/1.0; +https://flexoptix.net)", + Accept: "application/rss+xml, application/xml, text/xml", + }, + signal: AbortSignal.timeout(15000), + }); + + if (!resp.ok) { + console.warn(` Feed ${feed.name} returned ${resp.status}`); + return []; + } + + const rawXml = await resp.text(); + // Sanitize common RSS issues: unescaped & in URLs, attribute-without-value + const xml = rawXml + .replace(/&(?!amp;|lt;|gt;|quot;|apos;|#\d+;|#x[\dA-Fa-f]+;)/g, "&") + .replace(/(<\w[^>]*)\s+(\w+)=([^"'\s>]+)(?=[\s/>])/g, '$1 $2="$3"'); + const parsed = await parseStringPromise(xml, { explicitArray: false, strict: false }); + + // strict: false makes keys uppercase; support both + const rss = parsed?.rss || parsed?.RSS; + const channel = rss?.channel || rss?.CHANNEL || parsed?.feed || parsed?.FEED; + if (!channel) return []; + + const items = channel.item || channel.ITEM || channel.entry || channel.ENTRY || []; + const itemArray = Array.isArray(items) ? items : [items]; + + for (const item of itemArray) { + const title = extractText(item.title || item.TITLE) || ""; + const url = extractLink(item) || ""; + const summary = extractText( + item.description || item.DESCRIPTION || item.summary || item.SUMMARY || item["content:encoded"] + ) || ""; + const pubDate = item.pubDate || item.PUBDATE || item.published || item.updated || ""; + + if (!title || !url) continue; + + const publishedAt = pubDate ? new Date(pubDate) : new Date(); + if (isNaN(publishedAt.getTime())) continue; + + // Skip articles older than 7 days + const ageMs = Date.now() - publishedAt.getTime(); + if (ageMs > 7 * 24 * 60 * 60 * 1000) continue; + + const relevanceScore = scoreRelevance(title, summary); + const hash = contentHash({ title, url }); + + articles.push({ + title: title.slice(0, 500), + sourceUrl: url.slice(0, 1000), + summary: stripHtml(summary).slice(0, 2000), + publishedAt, + source: feed.name, + category: feed.category as NewsCategory, + relevanceScore, + contentHash: hash, + }); + } + } catch (err) { + console.warn(` Feed ${feed.name} error:`, (err as Error).message); + } + + return articles; +} + +function extractText(value: unknown): string { + if (!value) return ""; + if (typeof value === "string") return value; + if (typeof value === "object" && value !== null) { + const obj = value as Record; + return String(obj._ || obj["#text"] || ""); + } + return String(value); +} + +function extractLink(item: Record): string { + const link = item.link || item.LINK; + if (typeof link === "string") return link; + if (Array.isArray(link)) { + const rel = (link as Array>).find( + (l) => !l["$"] || (l["$"] as Record).rel === "alternate" + ); + return String((rel?.["$"] as Record)?.href || rel?._ || ""); + } + if (typeof link === "object" && link !== null) { + const l = link as Record; + return String((l["$"] as Record)?.href || l._ || ""); + } + return ""; +} + +function stripHtml(html: string): string { + return html + .replace(/<[^>]+>/g, " ") + .replace(/ /g, " ") + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, '"') + .replace(/\s+/g, " ") + .trim(); +} + +async function upsertArticle(article: NewsArticle): Promise { + const result = await pool.query( + `INSERT INTO news_articles (title, source_url, summary, published_at, source, category, relevance_score, content_hash) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + ON CONFLICT (source_url) DO UPDATE + SET relevance_score = EXCLUDED.relevance_score, + content_hash = EXCLUDED.content_hash + RETURNING (xmax = 0) AS inserted`, + [ + article.title, + article.sourceUrl, + article.summary, + article.publishedAt, + article.source, + article.category, + article.relevanceScore, + article.contentHash, + ] + ); + return result.rows[0]?.inserted ?? true; +} + +export async function scrapeNews(): Promise { + console.log("=== News Scraper Starting ===\n"); + + let totalFetched = 0; + let totalWritten = 0; + let totalRelevant = 0; + + for (const feed of FEEDS) { + console.log(`Fetching: ${feed.name} (${feed.url})`); + const articles = await fetchFeed(feed); + console.log(` → ${articles.length} articles (last 7 days)`); + + for (const article of articles) { + totalFetched++; + if (article.relevanceScore > 0) totalRelevant++; + + try { + const isNew = await upsertArticle(article); + if (isNew) totalWritten++; + } catch (err) { + console.error(` Error saving article:`, (err as Error).message); + } + } + + // Rate limit between feeds + await new Promise((r) => setTimeout(r, 1000)); + } + + console.log(`\nFetched: ${totalFetched} articles`); + console.log(`Relevant (score > 0): ${totalRelevant}`); + console.log(`Written: ${totalWritten} new`); + console.log("=== News Scraper Complete ===\n"); +} + +if (require.main === module) { + scrapeNews() + .then(() => pool.end()) + .catch((err) => { + console.error("Fatal:", err); + pool.end(); + process.exit(1); + }); +} diff --git a/packages/scraper/src/scrapers/optcore.ts b/packages/scraper/src/scrapers/optcore.ts new file mode 100644 index 0000000..5f69148 --- /dev/null +++ b/packages/scraper/src/scrapers/optcore.ts @@ -0,0 +1,297 @@ +/** + * Optcore.net Scraper — Most transparent pricing in the industry. + * Prices start at $5.50, fully public, no bot protection. + * + * Strategy: WP REST API to enumerate transceiver product URLs, + * then PlaywrightCrawler to render each page and extract price. + * + * Optcore uses Flatsome WooCommerce with Cloudflare Rocket Loader + * (JS lazy-loading) — static HTML has no product data. + */ +import { PlaywrightCrawler } from "crawlee"; +import { ensureVendor, upsertPriceObservation, findOrCreateScrapedTransceiver, pool } from "../utils/db"; +import { contentHash, parsePrice, parseStockLevel } from "../utils/hash"; + +const BASE_URL = "https://www.optcore.net"; + +// Transceiver category IDs from /wp-json/wp/v2/product_cat +// Filtered to optical transceiver categories with products +const TRANSCEIVER_CATEGORY_IDS = [ + 309, // 10G SFP+ + 173, // 1G SFP + 76, // 100G QSFP28 + 79, // 25G SFP28 + 73, // 40G QSFP+ + 311, // 10G BiDi SFP+ + 313, // 10G CWDM SFP+ + 312, // 10G DWDM SFP+ + 333, // 10G XFP + 1088, // 10GBase-T SFP+ + 59, // 8G/10G/16G SFP+ + 1102, // BiDi SFP + 4097, // 400G QSFP-DD + 77, // 100G CFP/CFP2/CFP4 + 4101, // 200G QSFP56 + 4092, // 50G SFP56 + 6441, // 800G OSFP +]; + +interface OptcoreProduct { + partNumber: string; + name: string; + price: number; + currency: string; + stockLevel: string; + url: string; + formFactor?: string; + speedGbps?: number; + speed?: string; + reachLabel?: string; +} + +function detectFormFactor(text: string): string | undefined { + const lower = text.toLowerCase(); + if (lower.includes("osfp") && !lower.includes("qsfp")) return "OSFP"; + if (lower.includes("qsfp-dd")) return "QSFP-DD"; + if (lower.includes("qsfp56")) return "QSFP56"; + if (lower.includes("qsfp28")) return "QSFP28"; + if (lower.includes("qsfp+") || lower.includes("qsfp plus")) return "QSFP+"; + if (lower.includes("sfp28")) return "SFP28"; + if (lower.includes("sfp56")) return "SFP56"; + if (lower.includes("sfp+") || lower.includes("sfp plus")) return "SFP+"; + if (lower.includes("cfp4")) return "CFP4"; + if (lower.includes("cfp2")) return "CFP2"; + if (lower.includes("cfp")) return "CFP"; + if (lower.includes("xfp")) return "XFP"; + if (lower.includes("sfp") && !lower.includes("qsfp")) return "SFP"; + return undefined; +} + +function detectSpeed(text: string): { speed: string; speedGbps: number } | undefined { + const patterns: [RegExp, string, number][] = [ + [/800\s*g/i, "800G", 800], + [/400\s*g/i, "400G", 400], + [/200\s*g/i, "200G", 200], + [/100\s*g/i, "100G", 100], + [/50\s*g/i, "50G", 50], + [/40\s*g/i, "40G", 40], + [/25\s*g/i, "25G", 25], + [/16\s*g/i, "16G", 16], + [/10\s*g/i, "10G", 10], + [/1000\s*base/i, "1G", 1], + [/1\s*g\b/i, "1G", 1], + ]; + for (const [re, speed, gbps] of patterns) { + if (re.test(text)) return { speed, speedGbps: gbps }; + } + return undefined; +} + +function detectReach(text: string): string | undefined { + const match = text.match(/(\d+)\s*(m|km)\b/i); + if (match) return `${match[1]}${match[2].toLowerCase()}`; + return undefined; +} + +/** + * Fetch product URLs for transceiver categories via WP REST API. + * Returns up to 2000 product URLs with title + slug for metadata. + */ +async function fetchTransceiverUrls(): Promise> { + const results: Array<{ url: string; title: string; partNumber: string }> = []; + const seen = new Set(); + + for (const catId of TRANSCEIVER_CATEGORY_IDS) { + let page = 1; + let hasMore = true; + + while (hasMore) { + const apiUrl = `${BASE_URL}/wp-json/wp/v2/product?product_cat=${catId}&per_page=100&page=${page}&_fields=slug,link,title`; + try { + const resp = await fetch(apiUrl, { + headers: { "User-Agent": "Mozilla/5.0 (compatible; TIP-Scraper/1.0)" }, + signal: AbortSignal.timeout(15000), + }); + + if (!resp.ok) break; + + const totalPages = parseInt(resp.headers.get("X-WP-TotalPages") || "1"); + const products: Array<{ slug: string; link: string; title: { rendered: string } }> = await resp.json(); + + for (const p of products) { + if (!seen.has(p.slug)) { + seen.add(p.slug); + results.push({ + url: p.link, + title: p.title.rendered, + partNumber: p.slug, + }); + } + } + + hasMore = page < totalPages; + page++; + + // Rate limit: 10 req/sec max + await new Promise((r) => setTimeout(r, 100)); + } catch { + hasMore = false; + } + } + } + + return results; +} + +export async function scrapeOptcore(): Promise { + console.log("=== Optcore.net Scraper Starting ===\n"); + + const vendorId = await ensureVendor( + "Optcore", + "compatible", + "https://www.optcore.net", + "https://www.optcore.net/product-category/optical-transceiver/" + ); + console.log(`Vendor ID: ${vendorId}`); + + // Step 1: Enumerate transceiver product URLs via WP REST API + console.log("Fetching product URLs via WP REST API..."); + const productMeta = await fetchTransceiverUrls(); + console.log(`Found ${productMeta.length} transceiver product URLs`); + + // Build a map for quick metadata lookup + const metaByUrl = new Map(productMeta.map((p) => [p.url, p])); + + const products: OptcoreProduct[] = []; + let pagesScraped = 0; + + // Step 2: Render each product page with Playwright to extract price + const crawler = new PlaywrightCrawler({ + maxConcurrency: 3, + maxRequestsPerMinute: 30, + requestHandlerTimeoutSecs: 30, + headless: true, + launchContext: { + launchOptions: { + args: ["--disable-blink-features=AutomationControlled", "--no-sandbox"], + }, + }, + + async requestHandler({ page, request, log }) { + const url = request.url; + log.info(`Scraping: ${url}`); + + // Wait for WooCommerce price element to appear + try { + await page.waitForSelector(".woocommerce-Price-amount, .price .amount, [class*=\"price\"]", { + timeout: 8000, + }); + } catch { + // Price element not found — might be out of stock or JS failed + log.warning(`No price element found: ${url}`); + pagesScraped++; + return; + } + + const data = await page.evaluate(() => { + // Product title + const title = + document.querySelector("h1.product_title, h1.entry-title, h1")?.textContent?.trim() || ""; + + // Price — WooCommerce renders: $5.50 + const priceEl = document.querySelector( + ".price ins .woocommerce-Price-amount, .price .woocommerce-Price-amount, .woocommerce-Price-amount" + ); + const priceText = priceEl?.textContent?.trim() || ""; + + // Stock + const stockEl = document.querySelector(".stock, .availability, [class*=\"stock\"]"); + const stockText = stockEl?.textContent?.trim() || ""; + + return { title, priceText, stockText }; + }); + + const meta = metaByUrl.get(url); + const name = data.title || meta?.title || url.split("/").filter(Boolean).pop() || ""; + const partNumber = meta?.partNumber || url.split("/").filter(Boolean).pop() || ""; + + const { price, currency } = parsePrice(data.priceText); + if (price > 0) { + const speedInfo = detectSpeed(name); + products.push({ + partNumber, + name, + price, + currency, + stockLevel: data.stockText ? parseStockLevel(data.stockText) : "in_stock", + url, + formFactor: detectFormFactor(name), + speedGbps: speedInfo?.speedGbps, + speed: speedInfo?.speed, + reachLabel: detectReach(name), + }); + } + + pagesScraped++; + }, + }); + + const urls = productMeta.map((p) => p.url); + await crawler.run(urls); + + console.log(`\nPages scraped: ${pagesScraped}`); + console.log(`Products with price: ${products.length}`); + + // Deduplicate + const unique = new Map(); + for (const p of products) { + if (!unique.has(p.partNumber)) unique.set(p.partNumber, p); + } + + // Write to DB + let written = 0; + let skipped = 0; + + for (const p of unique.values()) { + try { + const transceiverId = await findOrCreateScrapedTransceiver({ + partNumber: p.partNumber, + vendorId, + formFactor: p.formFactor, + speedGbps: p.speedGbps, + speed: p.speed, + reachLabel: p.reachLabel, + category: "DataCenter", + }); + + const hash = contentHash({ price: p.price, stock: p.stockLevel }); + const isNew = await upsertPriceObservation({ + transceiverId, + sourceVendorId: vendorId, + price: p.price, + currency: p.currency, + stockLevel: p.stockLevel, + url: p.url, + contentHash: hash, + }); + + if (isNew) written++; + else skipped++; + } catch (err) { + console.error(` Error: ${p.partNumber}:`, (err as Error).message); + } + } + + console.log(`\nDatabase: ${written} new, ${skipped} unchanged (${unique.size} unique)`); + console.log("=== Optcore.net Scraper Complete ===\n"); +} + +if (require.main === module) { + scrapeOptcore() + .then(() => pool.end()) + .catch((err) => { + console.error("Fatal:", err); + pool.end(); + process.exit(1); + }); +} diff --git a/packages/scraper/src/utils/db.ts b/packages/scraper/src/utils/db.ts new file mode 100644 index 0000000..3e6b9c0 --- /dev/null +++ b/packages/scraper/src/utils/db.ts @@ -0,0 +1,123 @@ +import { Pool } from "pg"; +import { config } from "dotenv"; +import { join } from "path"; + +config({ path: join(__dirname, "..", "..", "..", "..", ".env") }); + +export const pool = new Pool({ + host: process.env.POSTGRES_HOST || "localhost", + port: parseInt(process.env.POSTGRES_PORT || "5433"), + database: process.env.POSTGRES_DB || "transceiver_db", + user: process.env.POSTGRES_USER || "tip", + password: process.env.POSTGRES_PASSWORD || "tip_dev_2026", + max: 10, +}); + +export async function upsertPriceObservation(params: { + transceiverId: string; + sourceVendorId: string; + price: number; + currency: string; + stockLevel: string; + quantityAvailable?: number; + leadTimeDays?: number; + url?: string; + contentHash: string; +}): Promise { + // Check if price changed via content hash + const existing = await pool.query( + `SELECT content_hash FROM price_observations + WHERE transceiver_id = $1 AND source_vendor_id = $2 + ORDER BY time DESC LIMIT 1`, + [params.transceiverId, params.sourceVendorId] + ); + + if (existing.rows.length > 0 && existing.rows[0].content_hash === params.contentHash) { + return false; // No change + } + + await pool.query( + `INSERT INTO price_observations (time, transceiver_id, source_vendor_id, price, currency, stock_level, quantity_available, lead_time_days, url, content_hash) + VALUES (NOW(), $1, $2, $3, $4, $5, $6, $7, $8, $9)`, + [ + params.transceiverId, + params.sourceVendorId, + params.price, + params.currency, + params.stockLevel, + params.quantityAvailable || null, + params.leadTimeDays || null, + params.url || null, + params.contentHash, + ] + ); + return true; // New observation written +} + +export async function findOrCreateScrapedTransceiver(params: { + partNumber: string; + vendorId: string; + formFactor?: string; + speedGbps?: number; + speed?: string; + reachMeters?: number; + reachLabel?: string; + fiberType?: string; + wavelengths?: string; + category?: string; +}): Promise { + // Try to match existing transceiver by part number + vendor + const existing = await pool.query( + `SELECT id FROM transceivers WHERE part_number = $1 AND vendor_id = $2`, + [params.partNumber, params.vendorId] + ); + + if (existing.rows.length > 0) { + return existing.rows[0].id; + } + + // Create new transceiver entry + const slug = `scraped-${params.partNumber.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`; + const result = await pool.query( + `INSERT INTO transceivers (slug, part_number, vendor_id, form_factor, speed_gbps, speed, reach_meters, reach_label, fiber_type, wavelengths, category, market_status) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, 'Mainstream') + ON CONFLICT (slug) DO UPDATE SET updated_at = NOW() + RETURNING id`, + [ + slug, + params.partNumber, + params.vendorId, + params.formFactor || "SFP", + params.speedGbps || 0, + params.speed || "Unknown", + params.reachMeters || 0, + params.reachLabel || "", + params.fiberType || "", + params.wavelengths || "", + params.category || "DataCenter", + ] + ); + return result.rows[0].id; +} + +export async function getVendorId(name: string): Promise { + const result = await pool.query(`SELECT id FROM vendors WHERE name = $1`, [name]); + return result.rows[0]?.id || null; +} + +export async function ensureVendor( + name: string, + type: string, + website?: string, + shopUrl?: string +): Promise { + const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-"); + const result = await pool.query( + `INSERT INTO vendors (name, slug, type, website, shop_url, is_competitor) + VALUES ($1, $2, $3, $4, $5, true) + ON CONFLICT (name) DO UPDATE SET shop_url = COALESCE(EXCLUDED.shop_url, vendors.shop_url) + RETURNING id`, + [name, slug, type, website || null, shopUrl || null] + ); + return result.rows[0].id; +} diff --git a/packages/scraper/src/utils/hash.ts b/packages/scraper/src/utils/hash.ts new file mode 100644 index 0000000..44a4d8c --- /dev/null +++ b/packages/scraper/src/utils/hash.ts @@ -0,0 +1,71 @@ +import { createHash } from "crypto"; + +/** + * Generate SHA-256 content hash for change detection. + * Only hashes the fields that matter (price, stock, quantity). + */ +export function contentHash(data: Record): string { + const normalized = JSON.stringify(data, Object.keys(data).sort()); + return createHash("sha256").update(normalized).digest("hex").slice(0, 16); +} + +/** + * Parse price string into number. + * Handles: "$12.50", "12,50 €", "US$12.50", "12.50 USD" + */ +export function parsePrice(raw: string): { price: number; currency: string } { + const cleaned = raw.replace(/[^\d.,]/g, "").replace(",", "."); + const price = parseFloat(cleaned); + const currency = raw.includes("€") + ? "EUR" + : raw.includes("£") + ? "GBP" + : raw.includes("¥") + ? "CNY" + : "USD"; + return { price: isNaN(price) ? 0 : price, currency }; +} + +/** + * Determine stock level from various text representations. + */ +export function parseStockLevel( + raw: string +): "in_stock" | "low_stock" | "out_of_stock" | "on_request" | "discontinued" { + const lower = raw.toLowerCase(); + if (lower.includes("in stock") || lower.includes("auf lager") || lower.includes("available")) + return "in_stock"; + if (lower.includes("low stock") || lower.includes("few left") || lower.includes("limited")) + return "low_stock"; + if ( + lower.includes("out of stock") || + lower.includes("sold out") || + lower.includes("nicht verfügbar") || + lower.includes("unavailable") + ) + return "out_of_stock"; + if (lower.includes("discontinued") || lower.includes("eol") || lower.includes("end of life")) + return "discontinued"; + return "on_request"; +} + +/** + * Extract numeric quantity from stock text. + * "23 in stock" → 23, "500+ available" → 500 + */ +export function parseQuantity(raw: string): number | undefined { + const match = raw.match(/(\d+)\+?\s*(in stock|available|auf lager|stück|units|pcs)/i); + return match ? parseInt(match[1]) : undefined; +} + +/** + * Parse lead time from text. + * "Ships in 3-5 days" → 5, "2 weeks" → 14 + */ +export function parseLeadTime(raw: string): number | undefined { + const dayMatch = raw.match(/(\d+)\s*(business\s+)?days?/i); + if (dayMatch) return parseInt(dayMatch[1]); + const weekMatch = raw.match(/(\d+)\s*weeks?/i); + if (weekMatch) return parseInt(weekMatch[1]) * 7; + return undefined; +} diff --git a/packages/scraper/tsconfig.json b/packages/scraper/tsconfig.json new file mode 100644 index 0000000..87cde83 --- /dev/null +++ b/packages/scraper/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/scripts/migrate.ts b/scripts/migrate.ts new file mode 100644 index 0000000..6e54317 --- /dev/null +++ b/scripts/migrate.ts @@ -0,0 +1,42 @@ +import { readFileSync, readdirSync } from "fs"; +import { join } from "path"; +import { Pool } from "pg"; +import { config } from "dotenv"; + +config(); + +const pool = new Pool({ + host: process.env.POSTGRES_HOST || "localhost", + port: parseInt(process.env.POSTGRES_PORT || "5432"), + database: process.env.POSTGRES_DB || "transceiver_db", + user: process.env.POSTGRES_USER || "tip", + password: process.env.POSTGRES_PASSWORD || "tip_dev_2026", +}); + +async function migrate(): Promise { + const sqlDir = join(__dirname, "..", "sql"); + const files = readdirSync(sqlDir) + .filter((f) => f.endsWith(".sql")) + .sort(); + + console.log(`Found ${files.length} migration files`); + + const client = await pool.connect(); + try { + for (const file of files) { + const sql = readFileSync(join(sqlDir, file), "utf-8"); + console.log(`Running: ${file}...`); + await client.query(sql); + console.log(` Done: ${file}`); + } + console.log("\nAll migrations completed successfully."); + } catch (err) { + console.error("Migration failed:", err); + process.exit(1); + } finally { + client.release(); + await pool.end(); + } +} + +migrate(); diff --git a/scripts/seed-from-npm.ts b/scripts/seed-from-npm.ts new file mode 100644 index 0000000..527cbdb --- /dev/null +++ b/scripts/seed-from-npm.ts @@ -0,0 +1,280 @@ +/** + * Seed PostgreSQL from the existing @tip/core npm package data. + * Imports: 159 transceivers, 42 standards, 12 competitors, 11 breakouts. + */ +import { Pool } from "pg"; +import { config } from "dotenv"; +import { join } from "path"; + +config(); + +// Dynamic import of core package (ESM compat) +async function loadCoreData() { + const corePath = join(__dirname, "..", "packages", "core", "src"); + + // We need to use tsx to run this, so we can import .ts files directly + const { transceivers } = await import(join(corePath, "database")); + const { standards } = await import(join(corePath, "standards")); + const { competitors } = await import(join(corePath, "market")); + const { breakouts } = await import(join(corePath, "breakouts")); + + return { transceivers, standards, competitors, breakouts }; +} + +const pool = new Pool({ + host: process.env.POSTGRES_HOST || "localhost", + port: parseInt(process.env.POSTGRES_PORT || "5432"), + database: process.env.POSTGRES_DB || "transceiver_db", + user: process.env.POSTGRES_USER || "tip", + password: process.env.POSTGRES_PASSWORD || "tip_dev_2026", +}); + +function slugify(text: string): string { + return text + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-|-$/g, ""); +} + +async function seedVendors(client: any, competitors: readonly any[]): Promise> { + console.log("\nSeeding vendors..."); + const vendorIdMap = new Map(); + + // Insert Flexoptix first as primary vendor + const flexResult = await client.query( + `INSERT INTO vendors (name, slug, type, headquarters, country, website, shop_url, is_competitor, market_position, specialties, strengths, weaknesses) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) + ON CONFLICT (name) DO UPDATE SET updated_at = NOW() + RETURNING id`, + [ + "FLEXOPTIX", + "flexoptix", + "compatible", + "Mainz, Germany", + "Germany", + "https://www.flexoptix.net", + "https://www.flexoptix.net/en/", + false, + "Premium compatible optics with FlexBox programmer, 300+ vendor support", + ["compatible optics", "FlexBox", "all speeds", "premium quality"], + ["FlexBox programmer", "300+ vendor support", "lifetime warranty", "German engineering"], + [], + ] + ); + vendorIdMap.set("FLEXOPTIX", flexResult.rows[0].id); + console.log(" Inserted: FLEXOPTIX (primary)"); + + for (const comp of competitors) { + if (comp.name === "FLEXOPTIX") continue; // Already inserted + + const typeMap: Record = { + OEM: "oem", + Whitebox: "compatible", + Manufacturer: "manufacturer", + Distributor: "distributor", + Compatible: "compatible", + }; + + const result = await client.query( + `INSERT INTO vendors (name, slug, type, headquarters, is_competitor, market_position, specialties, strengths, weaknesses) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + ON CONFLICT (name) DO UPDATE SET updated_at = NOW() + RETURNING id`, + [ + comp.name, + slugify(comp.name), + typeMap[comp.type] || "oem", + comp.headquarters, + true, + comp.marketPosition, + [...comp.formFactorsOffered, ...comp.speedTiersOffered], + comp.strengths, + comp.weaknesses, + ] + ); + vendorIdMap.set(comp.name, result.rows[0].id); + console.log(` Inserted: ${comp.name}`); + } + + console.log(` Total vendors: ${vendorIdMap.size}`); + return vendorIdMap; +} + +async function seedStandards(client: any, standards: readonly any[]): Promise> { + console.log("\nSeeding standards..."); + const standardIdMap = new Map(); + + for (const std of standards) { + const body = std.ieeeReference?.startsWith("IEEE") + ? "IEEE" + : std.standard.includes("ZR") + ? "OIF" + : std.ieeeReference + ? "IEEE" + : "de_facto"; + + const result = await client.query( + `INSERT INTO standards (name, ieee_reference, body, speed, speed_gbps, lanes, lane_rate, modulation, fiber_type, wavelength, max_reach_meters, max_reach_label, connector, fec_required, form_factors, year_ratified, notes) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) + ON CONFLICT (name) DO UPDATE SET notes = EXCLUDED.notes + RETURNING id`, + [ + std.standard, + std.ieeeReference || null, + body, + std.speed, + parseFloat(std.speed) || null, + std.lanes, + std.laneRate, + std.modulation, + std.fiberType, + std.wavelength, + std.maxReachMeters, + std.maxReachLabel, + std.connector, + std.fecRequired, + std.formFactors, + std.yearRatified, + std.notes, + ] + ); + standardIdMap.set(std.standard, result.rows[0].id); + } + + console.log(` Total standards: ${standardIdMap.size}`); + return standardIdMap; +} + +async function seedTransceivers( + client: any, + transceivers: readonly any[], + standardIdMap: Map +): Promise { + console.log("\nSeeding transceivers..."); + let count = 0; + + for (const t of transceivers) { + const standardId = standardIdMap.get(t.standard) || null; + + // Detect WDM type + let wdmType = null; + if (t.category === "CWDM" || t.wavelengths?.includes("CWDM")) wdmType = "CWDM"; + if (t.category === "DWDM" || t.wavelengths?.includes("DWDM") || t.standard?.includes("DWDM")) + wdmType = "DWDM"; + + // Detect coherent + const coherent = + t.category === "Coherent" || + t.standard?.includes("ZR") || + t.modulation?.includes("DP-") || + false; + + await client.query( + `INSERT INTO transceivers ( + slug, standard_name, standard_id, ieee_reference, form_factor, + speed, speed_gbps, lanes, lane_rate, modulation, + reach_meters, reach_label, fiber_type, wavelengths, connector, + power_consumption_w, temp_range, category, price_tier, use_case, + vendor_compat, tags, generation, market_status, year_introduced, + breakout_capable, breakout_to, wdm_type, coherent + ) VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, + $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, + $21, $22, $23, $24, $25, $26, $27, $28, $29 + ) ON CONFLICT (slug) DO UPDATE SET updated_at = NOW()`, + [ + t.id, + t.standard, + standardId, + t.ieeeReference || null, + t.formFactor, + t.speed, + t.speedGbps, + t.lanes || null, + t.laneRate || null, + t.modulation || null, + t.reachMeters, + t.reachLabel, + t.fiberType, + t.wavelengths, + t.connector, + t.powerConsumptionW, + t.tempRange, + t.category, + t.priceTier, + t.useCase, + JSON.stringify(t.vendors), + t.tags, + t.generation || null, + t.marketStatus || "Mainstream", + t.yearIntroduced || null, + t.breakoutCapable || false, + t.breakoutTo || null, + wdmType, + coherent, + ] + ); + count++; + } + + console.log(` Total transceivers: ${count}`); +} + +async function seedBreakouts(client: any, breakouts: readonly any[]): Promise { + console.log("\nSeeding breakouts..."); + + for (const b of breakouts) { + await client.query( + `INSERT INTO breakouts (slug, from_standard, to_standard, form_factor, description, cable_type, max_length, speed_per_lane) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + ON CONFLICT (slug) DO UPDATE SET from_standard = $2`, + [b.id, b.from, b.to, b.formFactor, b.description, b.cableType, b.maxLength, b.speedPerLane] + ); + } + + console.log(` Total breakouts: ${breakouts.length}`); +} + +async function main(): Promise { + console.log("=== TIP Seed: Importing from @tip/core ===\n"); + + const { transceivers, standards, competitors, breakouts } = await loadCoreData(); + + console.log(`Source data: ${transceivers.length} transceivers, ${standards.length} standards, ${competitors.length} competitors, ${breakouts.length} breakouts`); + + const client = await pool.connect(); + try { + await client.query("BEGIN"); + + const vendorIdMap = await seedVendors(client, competitors); + const standardIdMap = await seedStandards(client, standards); + await seedTransceivers(client, transceivers, standardIdMap); + await seedBreakouts(client, breakouts); + + await client.query("COMMIT"); + console.log("\n=== Seed completed successfully ==="); + + // Print summary + const counts = await client.query(` + SELECT + (SELECT COUNT(*) FROM vendors) as vendors, + (SELECT COUNT(*) FROM standards) as standards, + (SELECT COUNT(*) FROM transceivers) as transceivers, + (SELECT COUNT(*) FROM breakouts) as breakouts + `); + console.log("\nDatabase summary:"); + console.log(` Vendors: ${counts.rows[0].vendors}`); + console.log(` Standards: ${counts.rows[0].standards}`); + console.log(` Transceivers: ${counts.rows[0].transceivers}`); + console.log(` Breakouts: ${counts.rows[0].breakouts}`); + } catch (err) { + await client.query("ROLLBACK"); + console.error("\nSeed failed:", err); + process.exit(1); + } finally { + client.release(); + await pool.end(); + } +} + +main(); diff --git a/scripts/setup-erik.sh b/scripts/setup-erik.sh new file mode 100755 index 0000000..86248d2 --- /dev/null +++ b/scripts/setup-erik.sh @@ -0,0 +1,73 @@ +#!/bin/bash +# TIP: Setup script for Erik server (.82) +# Run as root or with sudo + +set -euo pipefail + +echo "=== TIP: Erik Server Setup ===" +echo "" + +# 1. PostgreSQL 17 + TimescaleDB +echo "--- Installing PostgreSQL 17 + TimescaleDB ---" +apt-get update +apt-get install -y gnupg2 lsb-release + +# TimescaleDB repo (includes PostgreSQL 17) +echo "deb https://packagecloud.io/timescale/timescaledb/debian/ $(lsb_release -cs) main" > /etc/apt/sources.list.d/timescaledb.list +curl -fsSL https://packagecloud.io/timescale/timescaledb/gpgkey | gpg --dearmor -o /etc/apt/trusted.gpg.d/timescaledb.gpg + +apt-get update +apt-get install -y timescaledb-2-postgresql-17 + +# Enable TimescaleDB +timescaledb-tune --quiet --yes + +# pgvector +apt-get install -y postgresql-17-pgvector + +systemctl restart postgresql +systemctl enable postgresql + +# Create DB and user +sudo -u postgres psql < /opt/tip/app" +echo " 2. cd /opt/tip/app && npm install" +echo " 3. cp .env.example .env && edit .env" +echo " 4. npm run migrate" +echo " 5. npm run seed" +echo " 6. pm2 start ecosystem.config.js" diff --git a/sql/001-extensions.sql b/sql/001-extensions.sql new file mode 100644 index 0000000..f021889 --- /dev/null +++ b/sql/001-extensions.sql @@ -0,0 +1,7 @@ +-- TIP: Transceiver Intelligence Platform +-- Migration 001: Extensions + +CREATE EXTENSION IF NOT EXISTS timescaledb; +CREATE EXTENSION IF NOT EXISTS vector; -- pgvector +CREATE EXTENSION IF NOT EXISTS pg_trgm; -- Trigram similarity for fuzzy search +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- UUID generation diff --git a/sql/002-core-tables.sql b/sql/002-core-tables.sql new file mode 100644 index 0000000..96a6f01 --- /dev/null +++ b/sql/002-core-tables.sql @@ -0,0 +1,469 @@ +-- TIP: Transceiver Intelligence Platform +-- Migration 002: Core Tables + +-- ============================================================ +-- VENDORS (Hersteller, Distributoren, Reseller, OEMs) +-- ============================================================ +CREATE TABLE IF NOT EXISTS vendors ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL UNIQUE, + slug TEXT NOT NULL UNIQUE, + type TEXT NOT NULL CHECK (type IN ('manufacturer','distributor','oem','reseller','compatible')), + headquarters TEXT, + country TEXT, + website TEXT, + shop_url TEXT, + api_available BOOLEAN DEFAULT FALSE, + api_endpoint TEXT, + logo_r2_key TEXT, + founded_year INTEGER, + revenue_usd BIGINT, + employee_count INTEGER, + market_position TEXT, + specialties TEXT[] DEFAULT '{}', + scrape_config JSONB DEFAULT '{}', + last_scraped TIMESTAMPTZ, + is_competitor BOOLEAN DEFAULT FALSE, + is_factory BOOLEAN DEFAULT FALSE, + factory_locations TEXT[] DEFAULT '{}', + certifications TEXT[] DEFAULT '{}', + strengths TEXT[] DEFAULT '{}', + weaknesses TEXT[] DEFAULT '{}', + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- ============================================================ +-- STANDARDS (IEEE, OIF, MSA) +-- ============================================================ +CREATE TABLE IF NOT EXISTS standards ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL UNIQUE, + ieee_reference TEXT, + body TEXT CHECK (body IN ('IEEE','OIF','MSA','de_facto','proprietary')), + speed TEXT, + speed_gbps NUMERIC, + lanes INTEGER, + lane_rate TEXT, + lane_rate_gbps NUMERIC, + modulation TEXT, + fiber_type TEXT, + wavelength TEXT, + max_reach_meters INTEGER, + max_reach_label TEXT, + connector TEXT, + fec_required BOOLEAN DEFAULT FALSE, + form_factors TEXT[] DEFAULT '{}', + year_draft INTEGER, + year_ratified INTEGER, + year_revised INTEGER, + status TEXT DEFAULT 'ratified' CHECK (status IN ('draft','ratified','revised','superseded')), + superseded_by TEXT, + member_count INTEGER, + notes TEXT, + url TEXT, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- ============================================================ +-- TRANSCEIVERS +-- ============================================================ +CREATE TABLE IF NOT EXISTS transceivers ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + slug TEXT NOT NULL UNIQUE, + vendor_id UUID REFERENCES vendors(id), + part_number TEXT, + standard_name TEXT, + standard_id UUID REFERENCES standards(id), + ieee_reference TEXT, + form_factor TEXT NOT NULL, + speed TEXT NOT NULL, + speed_gbps NUMERIC NOT NULL, + lanes INTEGER, + lane_rate TEXT, + lane_rate_gbps NUMERIC, + modulation TEXT, + reach_meters INTEGER NOT NULL, + reach_label TEXT, + fiber_type TEXT, + wavelengths TEXT, + connector TEXT, + power_consumption_w NUMERIC, + temp_range TEXT DEFAULT 'COM' CHECK (temp_range IN ('COM','IND')), + category TEXT, + dom_support BOOLEAN DEFAULT TRUE, + digital_diagnostics TEXT, + + -- CWDM/DWDM + wdm_type TEXT CHECK (wdm_type IN ('CWDM','DWDM',NULL)), + channel_count INTEGER, + channel_spacing_ghz NUMERIC, + tunable BOOLEAN DEFAULT FALSE, + itu_grid TEXT, + + -- Coherent + coherent BOOLEAN DEFAULT FALSE, + baud_rate_gbaud NUMERIC, + fec_type TEXT, + dsp_vendor TEXT, + + -- Lifecycle + year_introduced INTEGER, + year_mainstream INTEGER, + year_peak INTEGER, + year_decline INTEGER, + market_status TEXT DEFAULT 'Mainstream' CHECK (market_status IN ('Mainstream','Growth','Emerging','Legacy','EOL')), + hype_cycle_phase TEXT, + generation TEXT, + + -- Pricing + price_tier TEXT CHECK (price_tier IN ('Budget','Standard','Premium')), + msrp_usd NUMERIC, + street_price_usd NUMERIC, + + -- Technical + optical_budget_db NUMERIC, + tx_power_min_dbm NUMERIC, + tx_power_max_dbm NUMERIC, + rx_sensitivity_dbm NUMERIC, + + -- Breakout + breakout_capable BOOLEAN DEFAULT FALSE, + breakout_to TEXT, + + -- Storage + datasheet_r2_key TEXT, + image_r2_key TEXT, + + -- Meta + use_case TEXT, + tags TEXT[] DEFAULT '{}', + vendor_compat JSONB DEFAULT '[]', + notes TEXT, + + -- Search vector (auto-populated by trigger) + search_vector TSVECTOR, + + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- ============================================================ +-- SWITCHES +-- ============================================================ +CREATE TABLE IF NOT EXISTS switches ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + vendor_id UUID REFERENCES vendors(id), + model TEXT NOT NULL, + series TEXT, + category TEXT CHECK (category IN ('DataCenter','Campus','Edge','Core','SP','Industrial')), + layer TEXT CHECK (layer IN ('L2','L3','L2/L3')), + managed BOOLEAN DEFAULT TRUE, + + -- Ports + ports_config JSONB DEFAULT '{}', + total_ports INTEGER, + uplink_speed_gbps NUMERIC, + max_speed_gbps NUMERIC, + + -- Performance + switching_capacity_tbps NUMERIC, + forwarding_rate_mpps NUMERIC, + latency_ns NUMERIC, + buffer_mb NUMERIC, + + -- ASIC + asic_vendor TEXT, + asic_model TEXT, + asic_generation TEXT, + + -- Features + poe_support TEXT DEFAULT 'None', + stacking_support BOOLEAN DEFAULT FALSE, + vxlan_support BOOLEAN DEFAULT FALSE, + evpn_support BOOLEAN DEFAULT FALSE, + bgp_support BOOLEAN DEFAULT FALSE, + mpls_support BOOLEAN DEFAULT FALSE, + openconfig_support BOOLEAN DEFAULT FALSE, + sonic_compatible BOOLEAN DEFAULT FALSE, + macsec_support BOOLEAN DEFAULT FALSE, + + -- Lifecycle + release_date DATE, + eos_date DATE, + eol_date DATE, + last_support_date DATE, + lifecycle_status TEXT DEFAULT 'Active' CHECK (lifecycle_status IN ('Active','EoS_Announced','EoL','Legacy')), + successor_model TEXT, + + -- Physical + rack_units NUMERIC, + max_power_w NUMERIC, + typical_power_w NUMERIC, + weight_kg NUMERIC, + airflow TEXT, + + -- Pricing + msrp_usd NUMERIC, + street_price_usd NUMERIC, + + -- Documentation + manual_r2_key TEXT, + datasheet_r2_key TEXT, + config_guide_r2_key TEXT, + compatibility_list_url TEXT, + + -- Meta + tags TEXT[] DEFAULT '{}', + search_vector TSVECTOR, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + + UNIQUE(vendor_id, model) +); + +-- ============================================================ +-- COMPATIBILITY (Switch <-> Transceiver) +-- ============================================================ +CREATE TABLE IF NOT EXISTS compatibility ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + switch_id UUID REFERENCES switches(id) ON DELETE CASCADE, + transceiver_id UUID REFERENCES transceivers(id) ON DELETE CASCADE, + verified_by TEXT, + verification_date DATE, + verification_method TEXT CHECK (verification_method IN ('tested','vendor_matrix','datasheet','community')), + status TEXT DEFAULT 'compatible' CHECK (status IN ('compatible','incompatible','partial','unknown')), + notes TEXT, + firmware_min TEXT, + known_issues TEXT, + source_url TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(switch_id, transceiver_id) +); + +-- ============================================================ +-- BREAKOUTS +-- ============================================================ +CREATE TABLE IF NOT EXISTS breakouts ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + slug TEXT NOT NULL UNIQUE, + from_standard TEXT NOT NULL, + to_standard TEXT NOT NULL, + form_factor TEXT, + description TEXT, + cable_type TEXT CHECK (cable_type IN ('Passive','Active')), + max_length TEXT, + speed_per_lane TEXT, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- ============================================================ +-- TEMPLATES (FlexBox Coding + Switch Config) +-- ============================================================ +CREATE TABLE IF NOT EXISTS templates ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + type TEXT NOT NULL CHECK (type IN ('flexbox_coding','switch_config')), + name TEXT NOT NULL, + description TEXT, + switch_vendor TEXT, + switch_series TEXT, + transceiver_type TEXT, + speed_gbps NUMERIC, + technology TEXT, + template_content TEXT NOT NULL, + variables JSONB DEFAULT '{}', + tags TEXT[] DEFAULT '{}', + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- ============================================================ +-- DOCUMENTS (PDFs in R2) +-- ============================================================ +CREATE TABLE IF NOT EXISTS documents ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + entity_type TEXT NOT NULL CHECK (entity_type IN ('transceiver','switch','vendor','standard')), + entity_id UUID, + doc_type TEXT NOT NULL CHECK (doc_type IN ('manual','datasheet','config_guide','compatibility_list','faq','whitepaper')), + title TEXT, + filename TEXT, + r2_key TEXT NOT NULL, + source_url TEXT, + file_size_bytes BIGINT, + page_count INTEGER, + ocr_status TEXT DEFAULT 'pending' CHECK (ocr_status IN ('pending','processing','completed','failed')), + ocr_text TEXT, + language TEXT DEFAULT 'en', + content_hash TEXT, + last_checked TIMESTAMPTZ, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- ============================================================ +-- KNOWLEDGE BASE (FAQs, Troubleshooting) +-- ============================================================ +CREATE TABLE IF NOT EXISTS knowledge_base ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + category TEXT NOT NULL CHECK (category IN ('troubleshooting','faq','best_practice','known_issue','compatibility_tip')), + subcategory TEXT, + question TEXT NOT NULL, + answer TEXT NOT NULL, + source_vendor TEXT, + source_url TEXT, + applies_to_form_factors TEXT[] DEFAULT '{}', + applies_to_speeds TEXT[] DEFAULT '{}', + applies_to_vendors TEXT[] DEFAULT '{}', + severity TEXT CHECK (severity IN ('critical','high','medium','low','info')), + resolution_steps JSONB, + last_verified TIMESTAMPTZ, + helpful_count INTEGER DEFAULT 0, + tags TEXT[] DEFAULT '{}', + search_vector TSVECTOR, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- ============================================================ +-- FACTORIES +-- ============================================================ +CREATE TABLE IF NOT EXISTS factories ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + vendor_id UUID REFERENCES vendors(id), + name TEXT NOT NULL, + country TEXT NOT NULL, + city TEXT, + factory_type TEXT CHECK (factory_type IN ('manufacturing','assembly','r_and_d','headquarters')), + products TEXT[] DEFAULT '{}', + capacity_units_month INTEGER, + employee_count INTEGER, + certifications TEXT[] DEFAULT '{}', + expansion_planned BOOLEAN DEFAULT FALSE, + expansion_details TEXT, + source_url TEXT, + last_verified TIMESTAMPTZ, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- ============================================================ +-- NEWS ARTICLES +-- ============================================================ +CREATE TABLE IF NOT EXISTS news_articles ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + title TEXT NOT NULL, + source TEXT NOT NULL, + source_url TEXT NOT NULL UNIQUE, + published_at TIMESTAMPTZ, + author TEXT, + summary TEXT, + full_text TEXT, + category TEXT CHECK (category IN ('product_launch','market_report','standard','m_and_a','factory','event')), + event TEXT, + mentioned_vendors TEXT[] DEFAULT '{}', + mentioned_products TEXT[] DEFAULT '{}', + mentioned_standards TEXT[] DEFAULT '{}', + sentiment_score NUMERIC, + relevance_score NUMERIC, + content_hash TEXT, + tags TEXT[] DEFAULT '{}', + search_vector TSVECTOR, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- ============================================================ +-- BLOG DRAFTS +-- ============================================================ +CREATE TABLE IF NOT EXISTS blog_drafts ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + title TEXT NOT NULL, + topic TEXT CHECK (topic IN ('hype_cycle','price_trend','new_product','comparison','tutorial')), + target_audience TEXT CHECK (target_audience IN ('sales','technical','customer','seo')), + outline JSONB, + draft_content TEXT, + data_sources JSONB, + status TEXT DEFAULT 'draft' CHECK (status IN ('draft','review','approved','published')), + generated_by TEXT, + word_count INTEGER, + seo_keywords TEXT[] DEFAULT '{}', + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- ============================================================ +-- TRIGGERS: Auto-update search_vector +-- ============================================================ + +-- Transceiver search vector +CREATE OR REPLACE FUNCTION transceivers_search_vector_update() RETURNS trigger AS $$ +BEGIN + NEW.search_vector := + setweight(to_tsvector('english', COALESCE(NEW.standard_name, '')), 'A') || + setweight(to_tsvector('english', COALESCE(NEW.form_factor, '')), 'A') || + setweight(to_tsvector('english', COALESCE(NEW.speed, '')), 'A') || + setweight(to_tsvector('english', COALESCE(NEW.use_case, '')), 'B') || + setweight(to_tsvector('english', COALESCE(NEW.category, '')), 'B') || + setweight(to_tsvector('english', COALESCE(NEW.wavelengths, '')), 'C') || + setweight(to_tsvector('english', COALESCE(NEW.modulation, '')), 'C') || + setweight(to_tsvector('english', COALESCE(NEW.generation, '')), 'C') || + setweight(to_tsvector('english', COALESCE(array_to_string(NEW.tags, ' '), '')), 'D'); + NEW.updated_at := NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER transceivers_search_update + BEFORE INSERT OR UPDATE ON transceivers + FOR EACH ROW EXECUTE FUNCTION transceivers_search_vector_update(); + +-- Switch search vector +CREATE OR REPLACE FUNCTION switches_search_vector_update() RETURNS trigger AS $$ +BEGIN + NEW.search_vector := + setweight(to_tsvector('english', COALESCE(NEW.model, '')), 'A') || + setweight(to_tsvector('english', COALESCE(NEW.series, '')), 'A') || + setweight(to_tsvector('english', COALESCE(NEW.category, '')), 'B') || + setweight(to_tsvector('english', COALESCE(NEW.asic_vendor, '')), 'C') || + setweight(to_tsvector('english', COALESCE(NEW.asic_model, '')), 'C') || + setweight(to_tsvector('english', COALESCE(array_to_string(NEW.tags, ' '), '')), 'D'); + NEW.updated_at := NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER switches_search_update + BEFORE INSERT OR UPDATE ON switches + FOR EACH ROW EXECUTE FUNCTION switches_search_vector_update(); + +-- Knowledge base search vector +CREATE OR REPLACE FUNCTION kb_search_vector_update() RETURNS trigger AS $$ +BEGIN + NEW.search_vector := + setweight(to_tsvector('english', COALESCE(NEW.question, '')), 'A') || + setweight(to_tsvector('english', COALESCE(NEW.answer, '')), 'B') || + setweight(to_tsvector('english', COALESCE(NEW.category, '')), 'C') || + setweight(to_tsvector('english', COALESCE(array_to_string(NEW.tags, ' '), '')), 'D'); + NEW.updated_at := NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER kb_search_update + BEFORE INSERT OR UPDATE ON knowledge_base + FOR EACH ROW EXECUTE FUNCTION kb_search_vector_update(); + +-- News search vector +CREATE OR REPLACE FUNCTION news_search_vector_update() RETURNS trigger AS $$ +BEGIN + NEW.search_vector := + setweight(to_tsvector('english', COALESCE(NEW.title, '')), 'A') || + setweight(to_tsvector('english', COALESCE(NEW.summary, '')), 'B') || + setweight(to_tsvector('english', COALESCE(NEW.source, '')), 'C') || + setweight(to_tsvector('english', COALESCE(array_to_string(NEW.tags, ' '), '')), 'D'); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER news_search_update + BEFORE INSERT OR UPDATE ON news_articles + FOR EACH ROW EXECUTE FUNCTION news_search_vector_update(); diff --git a/sql/003-timeseries.sql b/sql/003-timeseries.sql new file mode 100644 index 0000000..b737d87 --- /dev/null +++ b/sql/003-timeseries.sql @@ -0,0 +1,122 @@ +-- TIP: Transceiver Intelligence Platform +-- Migration 003: TimescaleDB Hypertables + +-- ============================================================ +-- PRICE OBSERVATIONS (Real-time competitor pricing) +-- ============================================================ +CREATE TABLE IF NOT EXISTS price_observations ( + time TIMESTAMPTZ NOT NULL, + transceiver_id UUID NOT NULL, + source_vendor_id UUID NOT NULL, + price NUMERIC NOT NULL, + currency TEXT DEFAULT 'USD', + stock_level TEXT CHECK (stock_level IN ('in_stock','low_stock','out_of_stock','on_request','discontinued')), + quantity_available INTEGER, + lead_time_days INTEGER, + min_order_qty INTEGER, + url TEXT, + content_hash TEXT +); + +SELECT create_hypertable('price_observations', by_range('time'), + if_not_exists => TRUE); + +-- ============================================================ +-- STOCK OBSERVATIONS (Separate stock tracking) +-- ============================================================ +CREATE TABLE IF NOT EXISTS stock_observations ( + time TIMESTAMPTZ NOT NULL, + transceiver_id UUID NOT NULL, + source_vendor_id UUID NOT NULL, + in_stock BOOLEAN NOT NULL, + quantity_available INTEGER, + lead_time_days INTEGER, + content_hash TEXT +); + +SELECT create_hypertable('stock_observations', by_range('time'), + if_not_exists => TRUE); + +-- ============================================================ +-- MARKET METRICS (Hype Cycle input data) +-- ============================================================ +CREATE TABLE IF NOT EXISTS market_metrics ( + time TIMESTAMPTZ NOT NULL, + technology TEXT NOT NULL, + metric_type TEXT NOT NULL CHECK (metric_type IN ( + 'vendor_count','shipment_share','asp_decline_rate', + 'media_hype_index','patent_filings','port_shipments', + 'revenue_usd','asp_usd' + )), + value NUMERIC NOT NULL, + source TEXT, + notes TEXT +); + +SELECT create_hypertable('market_metrics', by_range('time'), + if_not_exists => TRUE); + +-- ============================================================ +-- CONTINUOUS AGGREGATES +-- ============================================================ + +-- Daily price aggregates +CREATE MATERIALIZED VIEW IF NOT EXISTS price_daily +WITH (timescaledb.continuous) AS +SELECT + time_bucket('1 day', time) AS bucket, + transceiver_id, + source_vendor_id, + AVG(price) AS avg_price, + MIN(price) AS min_price, + MAX(price) AS max_price, + last(stock_level, time) AS latest_stock, + COUNT(*) AS observation_count +FROM price_observations +GROUP BY bucket, transceiver_id, source_vendor_id +WITH NO DATA; + +-- Weekly price aggregates +CREATE MATERIALIZED VIEW IF NOT EXISTS price_weekly +WITH (timescaledb.continuous) AS +SELECT + time_bucket('7 days', time) AS bucket, + transceiver_id, + source_vendor_id, + AVG(price) AS avg_price, + MIN(price) AS min_price, + MAX(price) AS max_price, + COUNT(*) AS observation_count +FROM price_observations +GROUP BY bucket, transceiver_id, source_vendor_id +WITH NO DATA; + +-- ============================================================ +-- RETENTION POLICIES +-- ============================================================ + +-- Raw price data: keep 90 days +SELECT add_retention_policy('price_observations', INTERVAL '90 days', + if_not_exists => TRUE); + +-- Raw stock data: keep 90 days +SELECT add_retention_policy('stock_observations', INTERVAL '90 days', + if_not_exists => TRUE); + +-- Market metrics: keep 10 years (small volume) +-- No retention policy needed + +-- ============================================================ +-- REFRESH POLICIES for continuous aggregates +-- ============================================================ +SELECT add_continuous_aggregate_policy('price_daily', + start_offset => INTERVAL '3 days', + end_offset => INTERVAL '1 hour', + schedule_interval => INTERVAL '1 hour', + if_not_exists => TRUE); + +SELECT add_continuous_aggregate_policy('price_weekly', + start_offset => INTERVAL '30 days', + end_offset => INTERVAL '7 days', + schedule_interval => INTERVAL '1 day', + if_not_exists => TRUE); diff --git a/sql/004-indexes.sql b/sql/004-indexes.sql new file mode 100644 index 0000000..4fdf9f9 --- /dev/null +++ b/sql/004-indexes.sql @@ -0,0 +1,75 @@ +-- TIP: Transceiver Intelligence Platform +-- Migration 004: Indexes + +-- ============================================================ +-- FULL-TEXT SEARCH INDEXES (GIN on tsvector) +-- ============================================================ +CREATE INDEX IF NOT EXISTS idx_transceivers_search ON transceivers USING GIN(search_vector); +CREATE INDEX IF NOT EXISTS idx_switches_search ON switches USING GIN(search_vector); +CREATE INDEX IF NOT EXISTS idx_kb_search ON knowledge_base USING GIN(search_vector); +CREATE INDEX IF NOT EXISTS idx_news_search ON news_articles USING GIN(search_vector); + +-- ============================================================ +-- ARRAY INDEXES (GIN on text[]) +-- ============================================================ +CREATE INDEX IF NOT EXISTS idx_transceivers_tags ON transceivers USING GIN(tags); +CREATE INDEX IF NOT EXISTS idx_switches_tags ON switches USING GIN(tags); +CREATE INDEX IF NOT EXISTS idx_vendors_specialties ON vendors USING GIN(specialties); +CREATE INDEX IF NOT EXISTS idx_standards_form_factors ON standards USING GIN(form_factors); +CREATE INDEX IF NOT EXISTS idx_kb_form_factors ON knowledge_base USING GIN(applies_to_form_factors); +CREATE INDEX IF NOT EXISTS idx_kb_speeds ON knowledge_base USING GIN(applies_to_speeds); +CREATE INDEX IF NOT EXISTS idx_news_vendors ON news_articles USING GIN(mentioned_vendors); + +-- ============================================================ +-- JSONB INDEXES +-- ============================================================ +CREATE INDEX IF NOT EXISTS idx_transceivers_vendor_compat ON transceivers USING GIN(vendor_compat); +CREATE INDEX IF NOT EXISTS idx_switches_ports ON switches USING GIN(ports_config); + +-- ============================================================ +-- B-TREE INDEXES (lookups, sorting) +-- ============================================================ +CREATE INDEX IF NOT EXISTS idx_transceivers_form_factor ON transceivers(form_factor); +CREATE INDEX IF NOT EXISTS idx_transceivers_speed_gbps ON transceivers(speed_gbps); +CREATE INDEX IF NOT EXISTS idx_transceivers_category ON transceivers(category); +CREATE INDEX IF NOT EXISTS idx_transceivers_market_status ON transceivers(market_status); +CREATE INDEX IF NOT EXISTS idx_transceivers_reach ON transceivers(reach_meters); +CREATE INDEX IF NOT EXISTS idx_transceivers_fiber_type ON transceivers(fiber_type); +CREATE INDEX IF NOT EXISTS idx_transceivers_wdm_type ON transceivers(wdm_type); +CREATE INDEX IF NOT EXISTS idx_transceivers_coherent ON transceivers(coherent); + +CREATE INDEX IF NOT EXISTS idx_switches_vendor ON switches(vendor_id); +CREATE INDEX IF NOT EXISTS idx_switches_category ON switches(category); +CREATE INDEX IF NOT EXISTS idx_switches_lifecycle ON switches(lifecycle_status); +CREATE INDEX IF NOT EXISTS idx_switches_max_speed ON switches(max_speed_gbps); + +CREATE INDEX IF NOT EXISTS idx_compatibility_switch ON compatibility(switch_id); +CREATE INDEX IF NOT EXISTS idx_compatibility_transceiver ON compatibility(transceiver_id); + +CREATE INDEX IF NOT EXISTS idx_documents_entity ON documents(entity_type, entity_id); +CREATE INDEX IF NOT EXISTS idx_documents_ocr_status ON documents(ocr_status); + +CREATE INDEX IF NOT EXISTS idx_kb_category ON knowledge_base(category); +CREATE INDEX IF NOT EXISTS idx_kb_severity ON knowledge_base(severity); + +CREATE INDEX IF NOT EXISTS idx_news_published ON news_articles(published_at DESC); +CREATE INDEX IF NOT EXISTS idx_news_category ON news_articles(category); +CREATE INDEX IF NOT EXISTS idx_news_event ON news_articles(event); + +CREATE INDEX IF NOT EXISTS idx_vendors_type ON vendors(type); +CREATE INDEX IF NOT EXISTS idx_vendors_competitor ON vendors(is_competitor) WHERE is_competitor = TRUE; + +-- ============================================================ +-- TRIGRAM INDEXES (fuzzy search on names) +-- ============================================================ +CREATE INDEX IF NOT EXISTS idx_vendors_name_trgm ON vendors USING GIN(name gin_trgm_ops); +CREATE INDEX IF NOT EXISTS idx_switches_model_trgm ON switches USING GIN(model gin_trgm_ops); +CREATE INDEX IF NOT EXISTS idx_standards_name_trgm ON standards USING GIN(name gin_trgm_ops); + +-- ============================================================ +-- TIMESERIES INDEXES +-- ============================================================ +CREATE INDEX IF NOT EXISTS idx_price_transceiver ON price_observations(transceiver_id, time DESC); +CREATE INDEX IF NOT EXISTS idx_price_source ON price_observations(source_vendor_id, time DESC); +CREATE INDEX IF NOT EXISTS idx_stock_transceiver ON stock_observations(transceiver_id, time DESC); +CREATE INDEX IF NOT EXISTS idx_metrics_technology ON market_metrics(technology, metric_type, time DESC);