diff --git a/README.md b/README.md index a615959..9c998ba 100644 --- a/README.md +++ b/README.md @@ -14,3 +14,44 @@ CanadianStartupJobs is built to strengthen Canada’s startup ecosystem by makin - Verified Canadian-owned startups only - Simple job posting and management dashboard for founders - Public API for regional innovation hubs and directories, such as university accelerators + +## Development Setup + +### Prerequisites +- Docker and Docker Compose +- Node.js (for backend services) + +### Database Setup + +Start PostgreSQL using Docker Compose: + +```bash +docker-compose up -d postgres +``` + +This will start a PostgreSQL 16 database with the following default configuration: +- **User**: `postgres` +- **Password**: `postgres` +- **Database**: `canadian_startup_jobs` +- **Port**: `5432` + +You can customize these settings by creating a `.env` file in the project root with: + +```env +POSTGRES_USER=your_user +POSTGRES_PASSWORD=your_password +POSTGRES_DB=your_database +POSTGRES_PORT=5432 +``` + +The database data will be persisted in a Docker volume. To stop the database: + +```bash +docker-compose down +``` + +To remove the database and all data: + +```bash +docker-compose down -v +``` \ No newline at end of file diff --git a/backend/resourceList.md b/backend/resourceList.md new file mode 100644 index 0000000..91360c1 --- /dev/null +++ b/backend/resourceList.md @@ -0,0 +1,166 @@ +## To Contact: +Startup Blink has the list and tons of other data (https://www.startupblink.com/startup-ecosystem/canada) +IRAP or IPON + +## Candidate Management System Links: +https://jobs.ashbyhq.com/ +https://.applytojob.com +https://jobs.lever.co/ + +## Company Careers Boards: +- https:///careers +- https:///jobs +- https:///en-ca/careers +- https:///en-ca/jobs + +## Master Lists: +- https://www.bycanada.tech/ +- https://www.canstarthub.ca/discover +- https://canadastartups.co/ +- https://growthlist.co/canada-startups/ + +## Smaller Programs Lists: +- https://drive.google.com/file/d/1Nx-0lyb_A24L6fJcHQm3AEnR_JFPE0OH/view +- https://drive.google.com/file/d/1lRe_ZtmlYzt89g_RAViWaBvFZG6tEJSU/view?pli=1 +- https://drive.google.com/file/d/110rHwUA_I0q4uUevgF46FBhyQ1t3uYuV/view +- https://drive.google.com/file/d/1JARpTeCVg4qjEhWMLUn8yhf9a8bQNMMn/view?usp=share_link +- https://cswaccelerator.com/cohort-2-2/ +- https://cswaccelerator.com/cohort1/ +- https://innovationfactory.ca/clients/ +- https://directory.nextcanada.com/directory/ventures/ +- https://startup.google.com/alumni/directory/?_gl=1*utxxg3*_up*MQ..*_ga*NTE2NDcyNDU3LjE3NjMwMTY1OTk.*_ga_GCB35PQ9X3*czE3NjMwMTY1OTgkbzEkZzAkdDE3NjMwMTY2MTQkajQ0JGwwJGgw®ion=canada +- https://velocityincubator.com/companies/ +- https://www.intuit.com/ca/prosperity-accelerator/alumni/ https://www.safexconnected.com/cohorts +- https://oneeleven-ca.squarespace.com/alumni // https://oneeleven-ca.squarespace.com/members +- https://dmz.torontomu.ca/startup-directory +- https://500.co/portfolio +- https://www1.communitech.ca/companies +- https://www.mcgill.ca/dobson/ourstartups +- https://www.ualberta.ca/en/medicine/research/health-innovation-hub/companies.html +- https://www.launchacademy.ca/alumni/ +- https://creativedestructionlab.com/companies/ +- https://www.founderfuel.com/companies/ +- https://www.yorku.ca/yspace/startups/ +- https://www.antler.co/portfolio +- https://entrepreneurs.utoronto.ca/our-startups/startups-directory/ +- https://innovation.ubc.ca/entrepreneurship-ventures/portfolio-companies +- https://www.acceleratorcentre.com/our-startups +- https://www.l-spark.com/meet-our-companies/ +- https://airtable.com/app1Z7w2CdfNJt9rB/shrM8IADuuZf3wvOU/tblxzzU3yUX1ru667 +- https://hatchery.engineering.utoronto.ca/our-startup-teams/ +- https://www.tandemlaunch.com/portfolio +- https://www.queensu.ca/innovationcentre/community/meet-startups +- https://www.investottawa.ca/io-accelerator-companies/ +- https://entrepreneurship.uwo.ca/accelerator/our-venture-directory/ +- https://platformcalgary.webflow.io/community?tab=partners +- https://icics.ubc.ca/hatch/alumni-hatch-ventures/ +- https://airtable.com/appurLNyQbHbSsCbJ/shrsOFwAHmVudJvsG/tbllsMtZKORBUYbXc +- https://www.launchlab.ca/client-stories +- https://edmontonunlimited.com/alumni-companies/ +- https://h2i.utoronto.ca/ventures/ +- https://foresightcac.com/companies +- https://www.citm.ca/clients/ +- https://www.torontomu.ca/zone-learning/legal-innovation-zone/startups/ +- https://www.torontomu.ca/zone-learning/legal-innovation-zone/startups/#!tab-1745414258423-alumni-startups +- https://www.torontomu.ca/zone-learning/biomedical-zone/startups1/ +- https://jnjinnovation.com/JLABSNavigator/ +- https://venturelabs.ca/companies/ +- https://www.ualberta.ca/en/business/alumni/business-directory.html +- https://spinup.utm.utoronto.ca/our-startups/ +- https://app.marsdd.com/directory +- https://mtlab.ca/startups/ +- https://lassonde.yorku.ca/best/startups/ +- https://www.district3.co/stories +- https://www.georgebrown.ca/startgbc/alumni-entrepreneurs +- https://www.plugandplaytechcenter.com/venture-capital/startup-portfolio +- https://www.torontomu.ca/zone-learning/sdz/startups/ +- https://www.torontomu.ca/zone-learning/fashion-zone/companies/ +- https://entrepreneurs.utoronto.ca/our-startups/startups-directory/ +- https://holtxchange.com/portfolio/ +- https://www.stationfintech.com/en/our-startups +- https://www.co-labs.ca/our-startups +- https://www.loi.vc/portfolio/ +- https://spring.is/impact-capital/portfolio/ +- https://www.torontomu.ca/svz/startups/ + +## VC Portfolios: +- + +Transmedia Zone (TMU) — Startups: https://www.torontomu.ca/transmedia-zone/startups/ +Startup Calgary — Showcase/Alumni (closest equivalent): https://www.startupcalgary.ca/launch-party +New Ventures BC — Alumni/Companies: https://www.newventuresbc.com/alumni +Global Startups — Portfolio: https://www.globalstartups.io/portfolio +Brampton Venture Zone (TMU) — Companies: https://www.bramptonventurezone.ca/companies +Ideas Inc (Saskatoon) — Companies: https://ideasinc.ca/companies/ +Foresight Canada — Ventures: https://foresightcac.com/ventures/ +Brampton Entrepreneur Centre — Homepage fallback: https://www.brampton.ca/EN/Business/BEC/Pages/Welcome.aspx +Parkdale Centre for Innovation — Companies/Portfolio: https://www.parkdaleinnovates.org/ +Centre for Entrepreneurship (U of T) — Homepage fallback (please clarify the specific unit) +Canada’s Tech Network — Members (closest equivalent): https://canadastechnetwork.ca/members/ +Digital Media and Gaming Incubator — Homepage fallback (please clarify which institution) +Startup Garage (uOttawa/Invest Ottawa) — Portfolio/Alumni: https://www.investottawa.ca/programs/startup-garage/ (closest equivalent) +Lazaridis ScaleUp Program — Companies: https://lazaridisinstitute.ca/scaleup/companies/ (if moved, main program page: https://lazaridisinstitute.ca/scaleup/) +Clean Energy Zone (TMU) — Companies/Startups: https://www.torontomu.ca/clean-energy-zone/ +ideaHUB — Homepage fallback (please clarify institution, e.g., Dalhousie vs. other) +Halton Region Small Business Centre — Homepage fallback: https://www.halton.ca/For-Business/Starting-a-Business/Small-Business-Centre +Centech (Montréal) — Startups: https://centech.co/startups/ +TechAlliance (Southwestern Ontario) — Client Directory: https://techalliance.ca/directory/ (closest equivalent) +CEED (Halifax) — Client stories/ventures (closest equivalent): https://www.ceed.ca/ +LiftOff — Homepage fallback (please clarify which program/org) +Design Fabrication Zone (TMU) — Startups: https://www.torontomu.ca/design-fabrication-zone/startups/ +The Forge (McMaster) — Companies: https://theforgehamilton.ca/companies/ +Innovation Boost Zone (TMU) — Startups: https://www.torontomu.ca/innovation-boost-zone/startups/ +La Piscine MTL — Startups/Portfolio: https://lapiscine.co/ (portfolio within site) +Canada’s Music Incubator — Programs/clients (closest equivalent): https://canadasmusicincubator.com/ +ACCEL (Centennial College) — Homepage fallback: https://www.centennialcollege.ca/centres-institutes/accel +Artscape Daniels Launchpad — Members/Residents (closest equivalent): https://www.artscape.ca/launchpad/ +Shad Canada — Alumni/success stories (closest equivalent): https://www.shad.ca/ +Canadian Technology Accelerators (TCS) — Programs/companies (closest equivalent): https://www.tradecommissioner.gc.ca/cta-atc/index.aspx?lang=eng +La base (HEC Montréal) — Startups/Portfolio: https://www.hec.ca/en/entrepreneurs/la-base/ +C100 — Fellows/Community (closest equivalent): https://www.thec100.org/fellows +North Forge — Clients/Startups: https://northforge.ca/clients/ +Altitude Accelerator — Ventures: https://altitudeaccelerator.ca/ventures/ +Brampton Venture Zone — Companies: https://www.bramptonventurezone.ca/companies +Rogers Cybersecure Catalyst — Accelerator Companies: https://www.torontomu.ca/cybersecure-catalyst/accelerator/companies/ +Brampton Entrepreneur Centre — Homepage: https://www.brampton.ca/EN/Business/BEC/Pages/Welcome.aspx +Bhive Brampton — Companies: https://www.bhive.ca/companies +TechPlace (Burlington) — Residents: https://techplace.ca/residents/ +Halton Region Small Business Centre — Homepage: https://www.halton.ca/For-Business/Starting-a-Business/Small-Business-Centre +Innovation Factory — Client Directory: https://innovationfactory.ca/client-directory/ +Alberta Catalyzer — Program page: https://www.albertacatalyzer.com/ +Plug and Play Alberta — Program page: https://www.plugandplaytechcenter.com/alberta/ +Alberta IoT Association — Directory: https://www.albertaiot.com/directory/ +Pacific Technology Ventures — Homepage (ambiguous): https://www.pacifictechnologyventures.com/ (please confirm) +Intrinsic Innovations — Homepage: https://www.intrinsicinnovations.ca/ +Creative Destruction Lab Calgary — Companies (global listing): https://creativedestructionlab.com/companies/ +REACH Canada — Program: https://www.reachinsurtech.com/canada +Foresight Canada — Ventures: https://foresightcac.com/ventures/ +Startup Calgary — Launch Party alumni: https://www.startupcalgary.ca/launch-party +Platform Calgary — Homepage: https://www.platformcalgary.com/ +C100 — Fellows/Community: https://www.thec100.org/fellows + +https://www.foundersbeta.com/startup-directory/categories/edmonton-startup-accelerators-incubators/ +https://www.foundersbeta.com/startup-directory/categories/guelph-startup-accelerators-incubators/ +https://www.foundersbeta.com/startup-directory/categories/halifax-startup-accelerators-incubators/ +https://www.foundersbeta.com/startup-directory/categories/hamilton-startup-accelerators-incubators/ +https://www.foundersbeta.com/startup-directory/categories/kingston-startup-accelerators-incubators/ +https://www.foundersbeta.com/startup-directory/categories/london-ontario-startup-accelerators-incubators/ +https://www.foundersbeta.com/startup-directory/categories/markham-startup-accelerators-incubators/ +https://www.foundersbeta.com/startup-directory/categories/mississauga-startup-accelerators-incubators/ +https://www.foundersbeta.com/startup-directory/categories/montreal-startup-accelerators-incubators/ +https://www.foundersbeta.com/startup-directory/categories/ottawa-startup-accelerators-incubators/ +https://www.foundersbeta.com/startup-directory/categories/startup-accelerators-and-incubators/ +https://www.foundersbeta.com/startup-directory/categories/toronto-startup-accelerators-incubators/ +https://www.foundersbeta.com/startup-directory/categories/vancouver-startup-accelerators-incubators/ +https://www.foundersbeta.com/startup-directory/categories/waterloo-startup-accelerators-incubators/ + +# Other Startup Resources +- eCommerce North: https://www.ecommercenorth.ca/ +- Volta Effect: https://www.voltaeffect.com/ +- Alberta Innovates: https://albertainnovates.ca/ +- TKSociety: https://www.theksociety.com/ +- Altitude Accelerator: https://altitudeaccelerator.ca/ventures/ +- TMU Cybersecure: https://cybersecurecatalyst.ca/catalyst-cyber-accelerator/ +- Bioenterprise Canada: https://bioenterprise.ca/member-directory/ +- Toronto Business Development Centre: https://tbdc.com/ +- The Forum: https://www.theforum.ca/ \ No newline at end of file diff --git a/backend/scraper-cron/.env.example b/backend/scraper-cron/.env.example new file mode 100644 index 0000000..780d075 --- /dev/null +++ b/backend/scraper-cron/.env.example @@ -0,0 +1,3 @@ +FIRE_CRAWL_API_KEY= +NODE_ENV=development +OPENAI_API_KEY= \ No newline at end of file diff --git a/backend/scraper-cron/README.md b/backend/scraper-cron/README.md new file mode 100644 index 0000000..642870b --- /dev/null +++ b/backend/scraper-cron/README.md @@ -0,0 +1,35 @@ +# Scraper Cron + +Service to scrape data from data sources as listed in resourceList.md + +## How It Works + +Scrapes company directories from `sources.ts`, discovers job board URLs, then batch scrapes them using Firecrawl with a structured job schema. Results are written to `new_jobs.json`. + +## Uses + +Used a mixture of playwright, open ai, and firecrawl to accomplish this. Firecrawl isn't great at pagination so we opted to use playwright + open ai for the heavy lifting. + +## Environment Variables + +Copy `.env.example` to `.env` and fill in: +- `FIRE_CRAWL_API_KEY` - Firecrawl API key for web scraping +- `OPENAI_API_KEY` - OpenAI API key for AI extraction +- `REDIS_URL` - Redis connection URL (or use `REDIS_HOST` and `REDIS_PORT`) + +## Startup + +```bash +npm install +npm run dev +``` + +Scrapes job boards from company directories and outputs to `new_jobs.json`. + + + + parallelizaton/queueing + setting this up to run on a cron + working onfault tolerance + saving to jobs to the db + filtering jobs/companies being parsed - probably most important to make sure they are actually startups. diff --git a/backend/scraper-cron/bull-board-server.ts b/backend/scraper-cron/bull-board-server.ts new file mode 100644 index 0000000..d6e2f23 --- /dev/null +++ b/backend/scraper-cron/bull-board-server.ts @@ -0,0 +1,43 @@ +import express from "express"; +import { createBullBoard } from "@bull-board/api"; +import { BullMQAdapter } from "@bull-board/api/bullMQAdapter"; +import { ExpressAdapter } from "@bull-board/express"; +import { + mapCompanyDirQueue, + companyDirectoryQueue, + jobBoardQueue, +} from "./queues"; +import dotenv from "dotenv"; + +dotenv.config(); + +const app = express(); + +// Create Express adapter for Bull Board +const serverAdapter = new ExpressAdapter(); +serverAdapter.setBasePath("/admin/queues"); + +// Create BullMQ Board with all queues +createBullBoard({ + queues: [ + new BullMQAdapter(mapCompanyDirQueue), + new BullMQAdapter(companyDirectoryQueue), + new BullMQAdapter(jobBoardQueue), + ], + serverAdapter, +}); + +// Mount BullMQ Board at the '/admin/queues' route +app.use("/admin/queues", serverAdapter.getRouter()); + +// Health check endpoint +app.get("/", (req, res) => { + res.redirect("/admin/queues"); +}); + +const PORT = process.env.BULL_BOARD_PORT || 3000; + +app.listen(PORT, () => { + // BullMQ Board running +}); + diff --git a/backend/scraper-cron/clear-queues.ts b/backend/scraper-cron/clear-queues.ts new file mode 100644 index 0000000..223e59e --- /dev/null +++ b/backend/scraper-cron/clear-queues.ts @@ -0,0 +1,49 @@ +// Simple script to clear all BullMQ queues +import { + mapCompanyDirQueue, + companyDirectoryQueue, + jobBoardQueue, + closeAllQueues, +} from "./queues"; +import { connectRedis } from "./redisClient"; +import dotenv from "dotenv"; + +dotenv.config(); + +const clearQueues = async () => { + await connectRedis(); + + const queues = [ + { name: "map-company-directories", queue: mapCompanyDirQueue }, + { name: "company-directories", queue: companyDirectoryQueue }, + { name: "job-boards", queue: jobBoardQueue }, + ]; + + for (const { name, queue } of queues) { + try { + // Get counts before clearing + const waiting = await queue.getWaitingCount(); + const active = await queue.getActiveCount(); + const completed = await queue.getCompletedCount(); + const failed = await queue.getFailedCount(); + const delayed = await queue.getDelayedCount(); + + const total = waiting + active + completed + failed + delayed; + + if (total > 0) { + // Clear all job states + await queue.obliterate({ force: true }); + } + } catch (error) { + // Error clearing queue + } + } + + await closeAllQueues(); + process.exit(0); +}; + +clearQueues().catch((error) => { + process.exit(1); +}); + diff --git a/backend/scraper-cron/constants.ts b/backend/scraper-cron/constants.ts new file mode 100644 index 0000000..b7610dd --- /dev/null +++ b/backend/scraper-cron/constants.ts @@ -0,0 +1,72 @@ +/** + * Application constants for workers, rate limiting, and batch processing + */ + +// ============================================================================ +// Worker Concurrency Settings +// ============================================================================ + +export const WORKER_CONCURRENCY = { + /** Process only 1 job board at a time to respect Firecrawl rate limits */ + JOB_BOARD: 10, + JOB_BOARD_BREADTH: 2, + /** Process 3 job extractions at a time */ + /** Process 2 company directory mappings in parallel */ + MAP_COMPANY_DIR: 2, + MAP_COMPANY_BREADTH: 2, + /** Process 2 company directories in parallel */ + COMPANY_DIRECTORY: 2, + COMPANY_DIRECTORY_BREADTH: 2, +} as const; + +// ============================================================================ +// Rate Limiter Settings +// ============================================================================ + +export const RATE_LIMITER = { + /** Firecrawl API rate limiting - 1 request per second */ + FIRECRAWL: { + max: 1, + duration: 1000, // milliseconds (1 second) + }, + /** OpenAI API rate limiting - 20 requests per minute */ + OPENAI: { + max: 20, + duration: 60000, // milliseconds (1 minute) + }, +} as const; + +// ============================================================================ +// Batch Processing Settings +// ============================================================================ + +export const BATCH_SETTINGS = { + /** Maximum number of links to process when mapping company directories */ + MAP_COMPANY_DIR_LIMIT: 50, + /** Delay between queue operations (milliseconds) */ + QUEUE_DELAY_MS: 2000, + /** Maximum items to process in a single batch for speed optimization */ + MAX_BATCH_SIZE: 2000, + /** Character limit for markdown chunks when processing content */ + MARKDOWN_CHUNK_CHARS: 10000, + /** Delay after clicking elements during scraping (milliseconds) */ + POST_CLICK_DELAY_MS: 1500, +} as const; + +// ============================================================================ +// Database Batch Settings +// ============================================================================ + +export const DB_BATCH_SETTINGS = { + /** Maximum number of jobs to process in a single database batch operation */ + MAX_JOBS_PER_BATCH: 1000, +} as const; + +// ============================================================================ +// Depth/Recursion Settings +// ============================================================================ + +export const RECURSION_SETTINGS = { + /** Maximum depth for company directory recursion */ + MAX_COMPANY_DIR_DEPTH: 1, +} as const; diff --git a/backend/scraper-cron/customScrapeAndExtract.ts b/backend/scraper-cron/customScrapeAndExtract.ts new file mode 100644 index 0000000..d1e031d --- /dev/null +++ b/backend/scraper-cron/customScrapeAndExtract.ts @@ -0,0 +1,646 @@ +// universalScraper.ts +import { chromium, Browser, Page } from "playwright"; +import { NodeHtmlMarkdown } from "node-html-markdown"; +import OpenAI from "openai"; +import dotenv from "dotenv"; +import crypto from "crypto"; +import { openaiClient } from "openaiClient"; +import { boolean } from "zod"; +dotenv.config(); + +function isAppendStyle(snapshots: PageSnapshot[]): boolean { + if (snapshots.length < 2) return false; + + const first = snapshots[0].markdown; + const last = snapshots[snapshots.length - 1].markdown; + + // if last page contains half or more of the first snapshot's HTML → append style + const slice = first.slice(0, Math.min(first.length, 2000)); // limit for speed + return last.includes(slice); +} + +/** + * Extract meaningful "blocks" from the HTML. + * This is extremely reliable for list/card-style pages. + */ + +function hashBlock(str: string): string { + return crypto.createHash("md5").update(str).digest("hex"); +} + +/** + * Main heuristic merger: + * - If append-style: return the last snapshot. + * - If paginated slices: dedupe blocks across all pages. + */ +function mergeSnapshotsHeuristically(snapshots: PageSnapshot[]): string { + if (snapshots.length === 0) return ""; + + // CASE 1 — Append-style pagination → last snapshot has everything + if (isAppendStyle(snapshots)) { + return snapshots[snapshots.length - 1].markdown.trim(); + } + + // CASE 2 — Paginated slices → merge unique blocks + const seen = new Set(); + const mergedBlocks: string[] = []; + + for (const snap of snapshots) { + const blocks = snap.markdown; + for (const block of blocks) { + const h = hashBlock(snap.markdown); + if (!seen.has(h)) { + seen.add(h); + mergedBlocks.push(block); + } + } + } + + const pageMarkDown = mergedBlocks.join(""); + return pageMarkDown; +} + +/** + * DROP-IN REPLACEMENT for your existing function. + * Eliminates all LLM calls and uses deterministic structural merging. + */ + +// ----------------------------- +// Types +// ----------------------------- + +interface Company { + name: string; + url: string; + isStartup?: boolean; +} + +interface CompanyDirectory { + directoryName: string; + url: string; +} + +interface JobBoard { + jobBoardName: string; + url: string; +} + +export interface ExtractionResult { + companies: Company[]; + companyDirectories: CompanyDirectory[]; + jobBoards: JobBoard[]; +} + +export interface GeneralScraperConfig { + url: string; + + /** + * Optional override for the main content container selector. + * If not provided, we'll detect it dynamically from the page. + */ + contentContainerSelector?: string; + + /** + * Optional override selector for pagination buttons/links. + * If not provided, we'll use a robust default built from keyword heuristics. + */ + paginationSelector?: string; + + /** + * Extra delay after each pagination click to let JS-heavy sites settle, in ms. + * Default: 1500 + */ + postClickDelayMs?: number; + + /** + * If true, we'll use an LLM post-pass to deduplicate repeated content + * across pages and keep only unique/useful sections. + */ + useLLMDiff?: boolean; +} + +interface PageSnapshot { + pageIndex: number; + url: string; + html: string; + markdown: string; +} + +// ----------------------------- +// Pagination selector plumbing +// ----------------------------- + +const PAGINATION_KEYWORDS = [ + "see more", + "show more", + "load more", + "more results", + "more", + "next", + "load older", + "older posts", + "load", + "continue", + "view more", +]; + +const buildDefaultPaginationSelector = (keywords: string[]): string => { + const buttonTextSelectors = keywords.map( + (k) => `button:has-text(${JSON.stringify(k)})` + ); + const linkTextSelectors = keywords.map( + (k) => `a:has-text(${JSON.stringify(k)})` + ); + const roleButtonTextSelectors = keywords.map( + (k) => `[role="button"]:has-text(${JSON.stringify(k)})` + ); + + const semanticNextSelectors = [ + `a[rel="next"]`, + `a[aria-label*="Next" i]`, + `button[aria-label*="Next" i]`, + `[role="button"][aria-label*="Next" i]`, + ]; + + const commonClassSelectors = [ + `button[id*="more" i]`, + `button[class*="more" i]`, + `button[class*="load" i]`, + `button[class*="next" i]`, + `button[class*="pagination" i]`, + `button[data-testid*="more" i]`, + `[role="button"][id*="more" i]`, + `[role="button"][class*="more" i]`, + `[role="button"][data-testid*="more" i]`, + ]; + + return [ + ...buttonTextSelectors, + ...linkTextSelectors, + ...roleButtonTextSelectors, + ...semanticNextSelectors, + ...commonClassSelectors, + ].join(", "); +}; + +// ----------------------------- +// HTML → Markdown helper +// ----------------------------- + +const htmlToMarkdown = (html: string): string => { + const strippedHtml = stripScriptAndStyleTags(html); + return NodeHtmlMarkdown.translate(strippedHtml); +}; + +export const stripScriptAndStyleTags = (html: string): string => { + return ( + html + // remove blocks + .replace(/]*>([\s\S]*?)<\/script>/gi, "") + // remove blocks + .replace(/]*>([\s\S]*?)<\/style>/gi, "") + // remove self-closing scripts (rare but valid) + .replace(/]*\/>/gi, "") + // remove self-closing styles + .replace(/]*\/>/gi, "") + ); +}; + +const paginateAndSnapshot = async ( + page: Page, + config: GeneralScraperConfig +): Promise => { + const { + paginationSelector: paginationSelectorOverride, + postClickDelayMs = 1500, + } = config; + + // 1. Detect container dynamically if not provided + const contentContainerSelector = "body"; + + const paginationSelector = + paginationSelectorOverride || + buildDefaultPaginationSelector(PAGINATION_KEYWORDS); + + const snapshots: PageSnapshot[] = []; + + let pageIndex = 0; + let lastHTML = ""; + let stableCount = 0; + + while (true) { + const iterationStart = Date.now(); + + // ---- 1. Snapshot current content ---- + const container = page.locator(contentContainerSelector); + const html = await container.innerHTML(); + const markdown = htmlToMarkdown(html); + + snapshots.push({ + pageIndex, + url: page.url(), + html, + markdown, + }); + if (pageIndex > 2) break; + + // ---- 2. Detect if content stopped changing ---- + if (html === lastHTML) { + stableCount++; + if (stableCount >= 3) { + break; + } + } else { + stableCount = 0; + } + lastHTML = html; + + // ---- 3. Check timeout for this loop iteration ---- + if (Date.now() - iterationStart > 20_000) { + break; + } + + // ---- 4. Look for 'Next' button ---- + const nextButton = page.locator(paginationSelector).first(); + + const isVisible = await nextButton.isVisible().catch(() => false); + if (!isVisible) { + break; + } + + // ---- 5. Check disabled state ---- + const isDisabled = await nextButton.isDisabled().catch(() => false); + const ariaDisabled = await nextButton.getAttribute("aria-disabled"); + + if (isDisabled || ariaDisabled === "true") { + break; + } + + // ---- 6. Click the button ---- + const beforeHTML = await container.innerHTML(); + + await nextButton.click({ force: true }); + + // ---- 7. Wait for content to change (with its own timeout) ---- + + const changed = await page + .waitForFunction( + (args) => { + const { sel, prev } = args; + const el = document.querySelector(sel); + return el && el.innerHTML !== prev; + }, + { sel: contentContainerSelector, prev: beforeHTML }, // <-- arg + { timeout: 8000 } // <-- options + ) + .then(() => true) + .catch(() => false); + + if (!changed) { + break; + } + + // ---- 8. Optional delay ---- + if (postClickDelayMs > 0) { + await page.waitForTimeout(postClickDelayMs); + } + + // ---- 9. Check if full iteration exceeded 20 sec ---- + if (Date.now() - iterationStart > 20_000) { + break; + } + + pageIndex++; + } + return snapshots; +}; + +// ----------------------------- +// OPTIONAL: LLM-based deduplication across pages +// ----------------------------- + +// ----------------------------- +// Tools for company / directory / job board extraction +// ----------------------------- + +const extractionTools: any[] = [ + { + type: "function", + function: { + name: "extract_entities", + description: + "Extract companies, company directories, and job boards from the given markdown.", + parameters: { + type: "object", + properties: { + companies: { + type: "array", + description: + "List of companies mentioned on the page with their main URL.", + items: { + type: "object", + properties: { + name: { type: "string" }, + url: { type: "string" }, + + isStartup: { + type: "boolean", + }, + }, + required: ["name", "url", "isStartup"], + }, + }, + companyDirectories: { + type: "array", + description: + "Startup/tech/company directories that list many companies.", + items: { + type: "object", + properties: { + directoryName: { type: "string" }, + url: { type: "string" }, + }, + required: ["directoryName", "url"], + }, + }, + jobBoards: { + type: "array", + description: + "Job boards or career listing pages relevant to the content.", + items: { + type: "object", + properties: { + jobBoardName: { type: "string" }, + url: { type: "string" }, + }, + required: ["jobBoardName", "url"], + }, + }, + }, + required: ["companies", "companyDirectories", "jobBoards"], + }, + }, + }, +]; + +// ----------------------------- +// High-level pipeline +// ----------------------------- + +export interface MarkdownSection { + heading: string; + level: number; + content: string; +} + +export const splitMarkdownByHeadings = ( + markdown: string, + chunkSize: number // number, not string +): MarkdownSection[] => { + const lines = markdown.split(/\r?\n/); + + const sections: MarkdownSection[] = []; + let current: MarkdownSection | null = null; + + const headingRegex = /^(#{1,6})\s+(.*)$/; + + for (const line of lines) { + const match = line.match(headingRegex); + + if (match) { + // Save previous section + if (current) sections.push(current); + + // Start a new one + const level = match[1].length; + const heading = match[2].trim(); + + current = { + heading, + level, + content: "", + }; + } else if (current) { + current.content += line + "\n"; + } + } + + if (current) sections.push(current); + + // + // SECOND PASS → split oversized sections by character count + // + const finalSections: MarkdownSection[] = []; + + for (const sec of sections) { + if (sec.content.length <= chunkSize) { + finalSections.push(sec); + continue; + } + + // Break into N chunks of size <= chunkSize + let start = 0; + const total = sec.content.length; + + while (start < total) { + const end = Math.min(start + chunkSize, total); + const chunkContent = sec.content.slice(start, end); + + finalSections.push({ + heading: sec.heading, + level: sec.level, + content: chunkContent, + }); + + start = end; + } + } + + return finalSections; +}; + +export const normalizeEntryToMarkdown = (entry: any): string => { + // 1. Use heading if available + const heading = entry.heading || entry.title || entry.name || "Entry"; + + // 2. Use content if exists + if (entry.content) { + return `### ${heading}\n\n${entry.content}\n\n`; + } + + // 3. Otherwise fallback to pretty-printed JSON + return `### ${heading}\n\n\`\`\`json\n${JSON.stringify( + entry, + null, + 2 + )}\n\`\`\`\n\n`; +}; + +export const chunkMarkdown = ( + sections: string[], + maxChars: number +): string[] => { + const chunks: string[] = []; + let current = ""; + + for (const sec of sections) { + if ((current + sec).length > maxChars) { + chunks.push(current); + current = sec; + } else { + current += sec; + } + } + + if (current.trim()) chunks.push(current); + return chunks; +}; + +export const mergeResults = (results: ExtractionResult[]): ExtractionResult => { + try { + let companies: Company[] = []; + let jobBoards: JobBoard[] = []; + let directories: CompanyDirectory[] = []; + for (const r of results) { + companies = companies.concat(r.companies); + jobBoards = jobBoards.concat(r.jobBoards); + directories = directories.concat(r.companyDirectories); + } + return { + companies, + jobBoards, + companyDirectories: directories, + }; + } catch (err) { + return { + companies: [], + jobBoards: [], + companyDirectories: [], + }; + } +}; +export const extractChunk = async ( + client: OpenAI, + markdownChunk: string +): Promise => { + try { + const completion = await client.chat.completions.create({ + model: "gpt-4.1-mini", + messages: [ + { + role: "system", + content: + "Extract all companies, job boards, and company directories from the following markdown, only return job boards if they really look like job boards, and aren't just companies or lists of companies.", + }, + { role: "user", content: markdownChunk }, + ], + tools: extractionTools, + tool_choice: "auto", + }); + + const toolCall = completion.choices?.[0]?.message?.tool_calls?.[0]; + + if (!toolCall) + return { + companies: [], + jobBoards: [], + companyDirectories: [], + }; + + const args = JSON.parse( + (toolCall as unknown as { function: { arguments: string } }).function + .arguments + ) as ExtractionResult; + + // Example fallback injection (your sample data) + // const args: ExtractionResult = { + // companies: [ + // { name: "Antler", url: "https://www.antler.co/location/canada" }, + // { name: "VClub", url: "https://vclub.poker" }, + // { name: "Brampton Angels", url: "https://bramptonangels.vc/" }, + // { name: "CVCA Intelligence", url: "https://intelligence.cvca.ca/" }, + // { + // name: "Canada Startup Association", + // url: "https://canadastartups.co/", + // }, + // ], + // companyDirectories: [], + // jobBoards: [], + // }; + + const companies: Company[] = []; + const jobBoards: JobBoard[] = []; + const companyDirectories: CompanyDirectory[] = []; + + if (Array.isArray(args.companies)) { + for (const c of args.companies) { + companies.push(c); + } + } + + if (Array.isArray(args.companyDirectories)) { + for (const d of args.companyDirectories) { + companyDirectories.push(d); + } + } + + if (Array.isArray(args.jobBoards)) { + for (const j of args.jobBoards) { + jobBoards.push(j); + } + } + + return { + companies, + jobBoards, + companyDirectories, + }; + } catch (err) { + return { + companies: [], + jobBoards: [], + companyDirectories: [], + }; + } +}; + +export const scrapeAndExtract = async ( + scraperConfig: GeneralScraperConfig +): Promise => { + const browser: Browser = await chromium.launch({ headless: true }); + const page: Page = await browser.newPage(); + const chunkChars = 10000; + + try { + await page.goto(scraperConfig.url, { waitUntil: "networkidle" }); + + const snapshots = await paginateAndSnapshot(page, scraperConfig); + const markdownCombined = mergeSnapshotsHeuristically(snapshots); + + const splitMarkDown = splitMarkdownByHeadings(markdownCombined, chunkChars); + + const markdownEntries = splitMarkDown.map(normalizeEntryToMarkdown); + const markdownChunks = chunkMarkdown(markdownEntries, chunkChars); + + const partialResults: ExtractionResult[] = []; + let i = 0; + for (const chunk of markdownChunks) { + i++; + const r = await extractChunk(openaiClient, chunk); + partialResults.push(r); + } + + // 4. Merge + const results = mergeResults(partialResults); + + return results; + } catch { + return { + companies: [], + companyDirectories: [], + jobBoards: [], + }; + } finally { + await browser.close(); + } +}; diff --git a/backend/scraper-cron/eslint.config.cjs b/backend/scraper-cron/eslint.config.cjs new file mode 100644 index 0000000..d9fa740 --- /dev/null +++ b/backend/scraper-cron/eslint.config.cjs @@ -0,0 +1,40 @@ +const { FlatCompat } = require("@eslint/eslintrc"); +const js = require("@eslint/js"); +const tsPlugin = require("@typescript-eslint/eslint-plugin"); +const tsParser = require("@typescript-eslint/parser"); + +const compat = new FlatCompat(); + +module.exports = [ + js.configs.recommended, + ...compat.extends( "prettier"), + { + files: ["**/*.{js,ts}"], + languageOptions: { + parser: tsParser, + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + + }, + }, + plugins: { + "@typescript-eslint": tsPlugin, + }, + rules: { + "@typescript-eslint/no-unused-vars": "error", + "prefer-const": "error", + "no-var": "error", + "@typescript-eslint/no-explicit-any": "error", + "no-console": ["error", { allow: ["warn", "error"] }], + "@typescript-eslint/ban-ts-comment": "warn", + }, + }, + { + files: ["**/charts/**/*.{js,ts}"], + rules: { + "@typescript-eslint/no-explicit-any": "off", + }, + ignores: ["**/charts/utils/**/*.{js,ts}"], + }, +]; diff --git a/backend/scraper-cron/eslint.config.mjs b/backend/scraper-cron/eslint.config.mjs new file mode 100644 index 0000000..05e726d --- /dev/null +++ b/backend/scraper-cron/eslint.config.mjs @@ -0,0 +1,18 @@ +import { defineConfig, globalIgnores } from "eslint/config"; +import nextVitals from "eslint-config-next/core-web-vitals"; +import nextTs from "eslint-config-next/typescript"; + +const eslintConfig = defineConfig([ + ...nextVitals, + ...nextTs, + // Override default ignores of eslint-config-next. + globalIgnores([ + // Default ignores of eslint-config-next: + ".next/**", + "out/**", + "build/**", + "next-env.d.ts", + ]), +]); + +export default eslintConfig; diff --git a/backend/scraper-cron/firecrawl.ts b/backend/scraper-cron/firecrawl.ts new file mode 100644 index 0000000..68eb620 --- /dev/null +++ b/backend/scraper-cron/firecrawl.ts @@ -0,0 +1,85 @@ +import Firecrawl, { + ActionOption, + FirecrawlClientOptions, +} from "@mendable/firecrawl-js"; +import dotenv from "dotenv"; +dotenv.config(); + +export const schema = { + type: "object", + properties: { + jobBoards: { + type: "array", + items: { + type: "object", + properties: { + jobBoardName: { type: "string" }, + url: { type: "string" }, + }, + required: ["jobBoardName", "url"], + }, + }, + companies: { + type: "array", + items: { + type: "object", + properties: { + companyName: { type: "string" }, + url: { type: "string" }, + isStartup: { type: "boolean" }, + }, + required: ["companyName", "url", "isCanadian"], + }, + }, + companyDirectories: { + type: "array", + items: { + type: "object", + properties: { + directoryName: { type: "string" }, + url: { type: "string" }, + }, + required: ["directoryName", "url"], + }, + }, + }, + required: ["jobBoards", "companies", "companyDirectories"], +}; + +export const jobSchema = { + type: "object", + properties: { + jobs: { + type: "array", + items: { + type: "object", + properties: { + title: { type: "string" }, + location: { type: "string" }, + remoteOk: { type: "boolean" }, + salaryMin: { type: "number" }, + salaryMax: { type: "number" }, + description: { type: "string" }, + company: { type: "string" }, + jobBoardUrl: { type: "string" }, + postingUrl: { type: "string" }, + isAtAStartup: { type: "boolean" }, + }, + required: [ + "title", + "location", + "remoteOk", + "description", + "company", + + // salaryMin and salaryMax intentionally omitted + ], + }, + }, + }, + required: ["jobs"], +}; + +export const firecrawl = new Firecrawl({ + apiKey: process.env.FIRE_CRAWL_API_KEY!, +}); diff --git a/backend/scraper-cron/index.ts b/backend/scraper-cron/index.ts new file mode 100644 index 0000000..001c75f --- /dev/null +++ b/backend/scraper-cron/index.ts @@ -0,0 +1,38 @@ +// will eventually boot cron job, currently just hitting the scraper directly + +import { writeFileSync } from "fs"; +import { mapCompanyDirQueue, closeAllQueues } from "queues"; +import { companyDirectoryUrls } from "sources"; +import { connectRedis } from "redisClient"; +import { WORKER_CONCURRENCY } from "./constants"; + +const getAllJobs = async () => { + await connectRedis(); + + // Initialize empty jobs file + writeFileSync("new_jobs.json", "[]"); + + // Add each initial company directory URL to the mapping queue + // Workers will map them in parallel and add discovered directories/job boards to other queues + const mapPromises = companyDirectoryUrls.map((url, index) => + index < WORKER_CONCURRENCY.MAP_COMPANY_BREADTH + ? mapCompanyDirQueue.add("map-company-directory", { url }) + : Promise.resolve() + ); + + const results = await Promise.allSettled(mapPromises); + const succeeded = results.filter((r) => r.status === "fulfilled").length; + const failed = results.filter((r) => r.status === "rejected").length; + + if (failed > 0) { + results.forEach((result, index) => { + if (result.status === "rejected") { + // Failed to add mapping job + } + }); + } +}; + +getAllJobs().catch((error) => { + process.exit(1); +}); diff --git a/backend/scraper-cron/new_jobs.json b/backend/scraper-cron/new_jobs.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/backend/scraper-cron/new_jobs.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/backend/scraper-cron/openaiClient.ts b/backend/scraper-cron/openaiClient.ts new file mode 100644 index 0000000..26966da --- /dev/null +++ b/backend/scraper-cron/openaiClient.ts @@ -0,0 +1,41 @@ +import { OpenAI } from "openai"; + +import dotenv from "dotenv"; +import { ChatCompletionTool } from "openai/resources"; +dotenv.config(); + +export const listingTool = { + type: "function", + function: { + name: "listing-extractor", + description: + "Tool to get single url for finding a job board or company directory from a site map", + parameters: { + type: "object", + properties: { + jobBoards: { + type: "array", + items: { + type: "string", + format: "url", + }, + description: + "A list of job board URLs. only show here if the url explicitly mentions job boards or career pages.", + }, + companyDirectories: { + type: "array", + items: { + type: "string", + format: "url", + }, + description: "A list of company directory URLs.", + }, + }, + required: ["jobBoards", "companyDirectories"], + }, + }, +} as ChatCompletionTool; + +export const openaiClient = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY!, +}); diff --git a/backend/scraper-cron/package-lock.json b/backend/scraper-cron/package-lock.json new file mode 100644 index 0000000..e4e308e --- /dev/null +++ b/backend/scraper-cron/package-lock.json @@ -0,0 +1,3194 @@ +{ + "name": "canadian-startup-jobs", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "canadian-startup-jobs", + "version": "0.1.0", + "dependencies": { + "@bull-board/api": "^6.14.2", + "@bull-board/express": "^6.14.2", + "@canadian-startup-jobs/db": "file:../../db", + "@mendable/firecrawl-js": "^4.6.2", + "@types/express": "^5.0.5", + "@types/node": "^24.10.1", + "bullmq": "^5.65.0", + "dotenv": "^17.2.3", + "drizzle-orm": "^0.36.4", + "express": "^5.1.0", + "htmlparser2": "^10.0.0", + "jsdom": "^27.2.0", + "node-html-markdown": "^2.0.0", + "openai": "^6.9.1", + "playwright": "^1.57.0", + "redis": "^4.7.0", + "tsx": "^4.20.6", + "zod": "^4.1.12" + }, + "devDependencies": { + "typescript": "^5" + } + }, + "../../db": { + "name": "@canadian-startup-jobs/db", + "version": "0.1.0", + "dependencies": { + "drizzle-orm": "^0.36.4", + "postgres": "^3.4.5" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "dotenv": "^17.2.3", + "drizzle-kit": "^0.30.0", + "typescript": "^5" + }, + "peerDependencies": { + "dotenv": "^17.2.3" + } + }, + "node_modules/@acemir/cssom": { + "version": "0.9.24", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.24.tgz", + "integrity": "sha512-5YjgMmAiT2rjJZU7XK1SNI7iqTy92DpaYVgG6x63FxkJ11UpYfLndHJATtinWJClAXiOlW9XWaUyAQf8pMrQPg==", + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.0.tgz", + "integrity": "sha512-9xiBAtLn4aNsa4mDnpovJvBn72tNEIACyvlqaNJ+ADemR+yeMJWnBudOi2qGDviJa7SwcDOU/TRh5dnET7qk0w==", + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "lru-cache": "^11.2.2" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.7.4", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.4.tgz", + "integrity": "sha512-buQDjkm+wDPXd6c13534URWZqbz0RP5PAhXZ+LIoa5LgwInT9HVJvGIJivg75vi8I13CxDGdTnz+aY5YUJlIAA==", + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.2" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "license": "MIT" + }, + "node_modules/@bull-board/api": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/@bull-board/api/-/api-6.14.2.tgz", + "integrity": "sha512-UzkvN/wM+1qS73BS43a75LYkRzpBpCCUKlaGq0hp3dM5MNmdF1mx7LMGYgXPt91gqF8j4jq9Y/zCpC3Sqs3RLQ==", + "license": "MIT", + "dependencies": { + "redis-info": "^3.1.0" + }, + "peerDependencies": { + "@bull-board/ui": "6.14.2" + } + }, + "node_modules/@bull-board/express": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/@bull-board/express/-/express-6.14.2.tgz", + "integrity": "sha512-nghb4MpYDodYZpeiZvI9tXFDHqiAXE8FhrLOFDkuQL0GBhw0gEOuGSISjdKrnFDAW72LWVq0XfGKWYD8V5nF0w==", + "license": "MIT", + "dependencies": { + "@bull-board/api": "6.14.2", + "@bull-board/ui": "6.14.2", + "ejs": "^3.1.10", + "express": "^4.21.1 || ^5.0.0" + } + }, + "node_modules/@bull-board/ui": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/@bull-board/ui/-/ui-6.14.2.tgz", + "integrity": "sha512-OTCsBbMAhYoB2NJc6FxkkREWWPUFvEhL2Az1gAKpdNOBqup4CsKj7eBK3rcWSRLZ4LnaOaPK8E8tiogkhrRuOA==", + "license": "MIT", + "dependencies": { + "@bull-board/api": "6.14.2" + } + }, + "node_modules/@canadian-startup-jobs/db": { + "resolved": "../../db", + "link": true + }, + "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-syntax-patches-for-csstree": { + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.19.tgz", + "integrity": "sha512-QW5/SM2ARltEhoKcmRI1LoLf3/C7dHGswwCnfLcoMgqurBT4f8GvwXMgAbK/FwcxthmJRK5MGTtddj0yQn0J9g==", + "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-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.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@ioredis/commands": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.4.0.tgz", + "integrity": "sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==", + "license": "MIT" + }, + "node_modules/@mendable/firecrawl-js": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/@mendable/firecrawl-js/-/firecrawl-js-4.6.2.tgz", + "integrity": "sha512-aSqZlL5sybe6VwIqCx78qWEWUmpjHwMcOBboGrOF5YULWCHBgUJbeuglUzeEzY/e+uExaR9wuxS+PWryVHtB+w==", + "license": "MIT", + "dependencies": { + "axios": "^1.12.2", + "typescript-event-target": "^1.1.1", + "ws": "^8.18.3", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.23.0" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@mendable/firecrawl-js/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" + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@redis/bloom": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", + "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/client": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz", + "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==", + "license": "MIT", + "dependencies": { + "cluster-key-slot": "1.1.2", + "generic-pool": "3.9.0", + "yallist": "4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@redis/graph": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz", + "integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/json": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.7.tgz", + "integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/search": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.2.0.tgz", + "integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/time-series": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.1.0.tgz", + "integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "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==", + "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==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.5.tgz", + "integrity": "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==", + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz", + "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "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==", + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "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/accepts/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/accepts/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/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/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "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/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/body-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", + "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.0", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "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/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/bullmq": { + "version": "5.65.0", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.65.0.tgz", + "integrity": "sha512-fyOcyf2ad4zrNmE18vdF/ie7DrW0TwhLt5e0DkqDxbRpDNiUdYqgp2QZJW2ntnUN08T2mDMC4deUUhF2UOAmeQ==", + "license": "MIT", + "dependencies": { + "cron-parser": "^4.9.0", + "ioredis": "^5.8.2", + "msgpackr": "^1.11.2", + "node-abort-controller": "^3.1.1", + "semver": "^7.5.4", + "tslib": "^2.0.0", + "uuid": "^11.1.0" + } + }, + "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/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/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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/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-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "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/cssstyle": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.3.tgz", + "integrity": "sha512-OytmFH+13/QXONJcC75QNdMtKpceNk3u8ThBjyyYjkEcy/ekBwR1mMAuNvi3gdBPW3N5TlCzQ0WZw8H0lN/bDw==", + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^4.0.3", + "@csstools/css-syntax-patches-for-csstree": "^1.0.14", + "css-tree": "^3.1.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz", + "integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==", + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "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/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "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/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "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/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/drizzle-orm": { + "version": "0.36.4", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.36.4.tgz", + "integrity": "sha512-1OZY3PXD7BR00Gl61UUOFihslDldfH4NFRH2MbP54Yxi0G/PKn4HfO65JYZ7c16DeP3SpM3Aw+VXVG9j6CRSXA==", + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=3", + "@electric-sql/pglite": ">=0.2.0", + "@libsql/client": ">=0.10.0", + "@libsql/client-wasm": ">=0.10.0", + "@neondatabase/serverless": ">=0.10.0", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/react": ">=18", + "@types/sql.js": "*", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=14.0.0", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "react": ">=18", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@libsql/client-wasm": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/react": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "react": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, + "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/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/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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/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/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "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/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/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.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/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/express/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/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "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": ">= 0.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/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/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "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/generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "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-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "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/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/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "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/htmlparser2": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", + "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", + "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.1", + "entities": "^6.0.0" + } + }, + "node_modules/htmlparser2/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/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/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.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/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/ioredis": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.8.2.tgz", + "integrity": "sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.4.0", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "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-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/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jsdom": { + "version": "27.2.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.2.0.tgz", + "integrity": "sha512-454TI39PeRDW1LgpyLPyURtB4Zx1tklSr6+OFOipsxGUH1WMTvk6C65JQdrj455+DP2uJ1+veBEHTGFKWVLFoA==", + "license": "MIT", + "dependencies": { + "@acemir/cssom": "^0.9.23", + "@asamuzakjp/dom-selector": "^6.7.4", + "cssstyle": "^5.3.3", + "data-urls": "^6.0.0", + "decimal.js": "^10.6.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", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.1.0", + "ws": "^8.18.3", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "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/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/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "license": "CC0-1.0" + }, + "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.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/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/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "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/msgpackr": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.5.tgz", + "integrity": "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==", + "license": "MIT", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } + }, + "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-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "license": "MIT" + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/node-html-markdown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/node-html-markdown/-/node-html-markdown-2.0.0.tgz", + "integrity": "sha512-DqUC3GGP7pwSYxS93SwHoP+qCw78xcMP6C6H2DuC8rPD2AweJRjBzQb5SdXpKtDlqAQ7hVotJcfhgU7hU5Gthw==", + "license": "MIT", + "dependencies": { + "node-html-parser": "^6.1.13" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/node-html-parser": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.13.tgz", + "integrity": "sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==", + "license": "MIT", + "dependencies": { + "css-select": "^5.1.0", + "he": "1.2.0" + } + }, + "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/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/openai": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.9.1.tgz", + "integrity": "sha512-vQ5Rlt0ZgB3/BNmTa7bIijYFhz3YBceAA3Z4JuoMSBftBF9YqFHIEhZakSs+O/Ad7EaoEimZvHxD5ylRjN11Lg==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "license": "MIT", + "dependencies": { + "entities": "^6.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/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/playwright": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "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/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-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/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.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/raw-body/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "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/redis": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.7.1.tgz", + "integrity": "sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ==", + "license": "MIT", + "workspaces": [ + "./packages/*" + ], + "dependencies": { + "@redis/bloom": "1.2.0", + "@redis/client": "1.6.1", + "@redis/graph": "1.1.1", + "@redis/json": "1.0.7", + "@redis/search": "1.2.0", + "@redis/time-series": "1.1.0" + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-info": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redis-info/-/redis-info-3.1.0.tgz", + "integrity": "sha512-ER4L9Sh/vm63DkIE0bkSjxluQlioBiBgf5w1UuldaW/3vPcecdljVDisZhmnCMvsxHNiARTTDDHGg9cGwTfrKg==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.11" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "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/router/node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/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/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/send/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/send/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/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "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/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, + "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/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/tldts": { + "version": "7.0.19", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.19.tgz", + "integrity": "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==", + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.19" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.19", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.19.tgz", + "integrity": "sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==", + "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/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "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.20.6", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", + "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "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/type-is/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/type-is/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/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-event-target": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/typescript-event-target/-/typescript-event-target-1.1.1.tgz", + "integrity": "sha512-dFSOFBKV6uwaloBCCUhxlD3Pr/P1a/tJdcmPrTXCHlEFD3faj0mztjcGn6VBAhQ0/Bdy8K3VWrrqwbt/ffsYsg==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "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/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "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/webidl-conversions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz", + "integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "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": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "license": "MIT", + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/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/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/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", + "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.0", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.0.tgz", + "integrity": "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } + } + } +} diff --git a/backend/scraper-cron/package.json b/backend/scraper-cron/package.json new file mode 100644 index 0000000..756ed98 --- /dev/null +++ b/backend/scraper-cron/package.json @@ -0,0 +1,37 @@ +{ + "name": "canadian-startup-jobs", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "tsx index.ts", + "test": "tsx test.ts", + "worker": "tsx workers/workers.ts", + "test-bullmq": "tsx test-bullmq.ts", + "clear-queues": "tsx clear-queues.ts", + "board": "tsx bull-board-server.ts" + }, + "dependencies": { + "@bull-board/api": "^6.14.2", + "@bull-board/express": "^6.14.2", + "@canadian-startup-jobs/db": "file:../../db", + "@mendable/firecrawl-js": "^4.6.2", + "drizzle-orm": "^0.36.4", + "@types/express": "^5.0.5", + "@types/node": "^24.10.1", + "bullmq": "^5.65.0", + "dotenv": "^17.2.3", + "express": "^5.1.0", + "htmlparser2": "^10.0.0", + "jsdom": "^27.2.0", + "node-html-markdown": "^2.0.0", + "openai": "^6.9.1", + "playwright": "^1.57.0", + "redis": "^4.7.0", + "tsx": "^4.20.6", + "zod": "^4.1.12" + }, + "devDependencies": { + "typescript": "^5" + }, + "packageManager": "pnpm@10.11.1+sha512.e519b9f7639869dc8d5c3c5dfef73b3f091094b0a006d7317353c72b124e80e1afd429732e28705ad6bfa1ee879c1fce46c128ccebd3192101f43dd67c667912" +} diff --git a/backend/scraper-cron/pagepath.html b/backend/scraper-cron/pagepath.html new file mode 100644 index 0000000..241685b --- /dev/null +++ b/backend/scraper-cron/pagepath.html @@ -0,0 +1,1100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +ByCanada.Tech - Coast-to-Coast Startup Directory + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/backend/scraper-cron/queues.ts b/backend/scraper-cron/queues.ts new file mode 100644 index 0000000..131a2b0 --- /dev/null +++ b/backend/scraper-cron/queues.ts @@ -0,0 +1,62 @@ +import { Queue, Worker, QueueEvents } from "bullmq"; +import dotenv from "dotenv"; +import { BATCH_SETTINGS } from "./constants"; + +dotenv.config(); + +// Redis connection configuration matching existing redisClient setup +export const redisConnection = process.env.REDIS_URL + ? { url: process.env.REDIS_URL } + : { + host: process.env.REDIS_HOST || "localhost", + port: parseInt(process.env.REDIS_PORT || "6379"), + }; + +// Queue for mapping company directories (initial discovery) +export const mapCompanyDirQueue = new Queue("map-company-directories", { + connection: redisConnection, +}); + +// Queue for scraping company directories +export const companyDirectoryQueue = new Queue("company-directories", { + connection: redisConnection, +}); + +// Queue for scraping job boards +export const jobBoardQueue = new Queue("job-boards", { + connection: redisConnection, + defaultJobOptions: { + attempts: 3, + backoff: { + type: "exponential", + delay: BATCH_SETTINGS.QUEUE_DELAY_MS, + }, + }, +}); + +// Queue events for monitoring +export const mapCompanyDirEvents = new QueueEvents("map-company-directories", { + connection: redisConnection, +}); + +export const companyDirectoryEvents = new QueueEvents("company-directories", { + connection: redisConnection, +}); + +export const jobBoardEvents = new QueueEvents("job-boards", { + connection: redisConnection, +}); + + +// Helper to close all queues +export const closeAllQueues = async () => { + await Promise.all([ + mapCompanyDirQueue.close(), + companyDirectoryQueue.close(), + jobBoardQueue.close(), + mapCompanyDirEvents.close(), + companyDirectoryEvents.close(), + jobBoardEvents.close() + ]); +}; + diff --git a/backend/scraper-cron/redisClient.ts b/backend/scraper-cron/redisClient.ts new file mode 100644 index 0000000..cbdf450 --- /dev/null +++ b/backend/scraper-cron/redisClient.ts @@ -0,0 +1,227 @@ +import { createClient } from "redis"; +import dotenv from "dotenv"; + +dotenv.config(); + +const redisClient = createClient({ + url: process.env.REDIS_URL || `redis://${process.env.REDIS_HOST || "localhost"}:${process.env.REDIS_PORT || 6379}`, + socket: { + reconnectStrategy: (retries: number) => { + if (retries > 10) { + return new Error("Redis connection failed"); + } + return Math.min(retries * 100, 3000); + }, + }, +}); + +redisClient.on("error", (err: Error) => { + // Redis Client Error +}); + +redisClient.on("connect", () => { + // Redis Client Connected +}); + +redisClient.on("ready", () => { + // Redis Client Ready +}); + +redisClient.on("reconnecting", () => { + // Redis Client Reconnecting +}); + +// Connect to Redis +export const connectRedis = async () => { + try { + if (!redisClient.isOpen) { + await redisClient.connect(); + } + } catch (error) { + throw error; + } +}; + +// Disconnect from Redis +export const disconnectRedis = async () => { + try { + if (redisClient.isOpen) { + await redisClient.quit(); + } + } catch (error) { + throw error; + } +}; + +// Export the client for direct use +export { redisClient }; + +// Helper functions for common operations +export const redisHelpers = { + // Get a value by key + get: async (key: string): Promise => { + try { + return await redisClient.get(key); + } catch (error) { + throw error; + } + }, + + // Set a value with optional expiration (in seconds) + set: async ( + key: string, + value: string, + options?: { EX?: number; NX?: boolean; XX?: boolean } + ): Promise => { + try { + const setOptions: { EX?: number; NX?: boolean; XX?: boolean } = {}; + if (options?.EX !== undefined) setOptions.EX = options.EX; + if (options?.NX !== undefined) setOptions.NX = options.NX; + if (options?.XX !== undefined) setOptions.XX = options.XX; + + if (Object.keys(setOptions).length > 0) { + return await redisClient.set(key, value, setOptions); + } + return await redisClient.set(key, value); + } catch (error) { + throw error; + } + }, + + // Delete a key + del: async (key: string | string[]): Promise => { + try { + return await redisClient.del(key); + } catch (error) { + throw error; + } + }, + + // Check if a key exists + exists: async (key: string | string[]): Promise => { + try { + return await redisClient.exists(key); + } catch (error) { + throw error; + } + }, + + // Set expiration on a key (in seconds) + expire: async (key: string, seconds: number): Promise => { + try { + return await redisClient.expire(key, seconds); + } catch (error) { + throw error; + } + }, + + // Get TTL (time to live) of a key + ttl: async (key: string): Promise => { + try { + return await redisClient.ttl(key); + } catch (error) { + throw error; + } + }, + + // Set a hash field + hSet: async ( + key: string, + field: string | Record, + value?: string + ): Promise => { + try { + if (typeof field === "string" && value !== undefined) { + return await redisClient.hSet(key, field, value); + } else if (typeof field === "object") { + return await redisClient.hSet(key, field); + } else { + throw new Error("Invalid arguments for hSet"); + } + } catch (error) { + throw error; + } + }, + + // Get a hash field + hGet: async (key: string, field: string): Promise => { + try { + return await redisClient.hGet(key, field); + } catch (error) { + throw error; + } + }, + + // Get all hash fields + hGetAll: async (key: string): Promise> => { + try { + return await redisClient.hGetAll(key); + } catch (error) { + throw error; + } + }, + + // Add to a set + sAdd: async (key: string, ...members: string[]): Promise => { + try { + return await redisClient.sAdd(key, members); + } catch (error) { + throw error; + } + }, + + // Check if member is in set + sIsMember: async (key: string, member: string): Promise => { + try { + return await redisClient.sIsMember(key, member); + } catch (error) { + throw error; + } + }, + + // Get all members of a set + sMembers: async (key: string): Promise => { + try { + return await redisClient.sMembers(key); + } catch (error) { + throw error; + } + }, + + // Push to a list (left) + lPush: async (key: string, ...values: string[]): Promise => { + try { + return await redisClient.lPush(key, values); + } catch (error) { + throw error; + } + }, + + // Push to a list (right) + rPush: async (key: string, ...values: string[]): Promise => { + try { + return await redisClient.rPush(key, values); + } catch (error) { + throw error; + } + }, + + // Pop from a list (left) + lPop: async (key: string): Promise => { + try { + return await redisClient.lPop(key); + } catch (error) { + throw error; + } + }, + + // Get list length + lLen: async (key: string): Promise => { + try { + return await redisClient.lLen(key); + } catch (error) { + throw error; + } + }, +}; + diff --git a/backend/scraper-cron/scrape-helpers/mapCompanyDirs.ts b/backend/scraper-cron/scrape-helpers/mapCompanyDirs.ts new file mode 100644 index 0000000..6b5d131 --- /dev/null +++ b/backend/scraper-cron/scrape-helpers/mapCompanyDirs.ts @@ -0,0 +1,78 @@ +import { ChatCompletion } from "openai/resources"; +import { firecrawl } from "../firecrawl"; +import { listingTool, openaiClient } from "../openaiClient"; +import { BATCH_SETTINGS } from "../constants"; + +export const mapCompanyDir = async ( + companyDirsToSearch: string[] +): Promise<{ + companyDirsCollectedByMap: string[]; + jobBoardsCollectedByMap: string[]; +}> => { + const companyDirsCollected: string[] = []; + const jobBoardsCollected: string[] = []; + + for (const companyDirToSearch of companyDirsToSearch) { + const result = await firecrawl.map(companyDirToSearch, { + limit: BATCH_SETTINGS.MAP_COMPANY_DIR_LIMIT, + sitemap: "include", + }); + + const response = await openaiClient.chat.completions.create({ + model: "gpt-4o", + messages: [ + { + role: "system", + content: + "You use the listing tool to extract urls with list of companies (known as company directories) and job boards from a map of this site. You choose one of each, you never return a sitemap", + }, + { + role: "user", + content: `Extract the company names and descriptions from the following links:\n\n${result.links + .map( + (link) => + ` ${link.url} + - ${link.title ?? "No Title"}: ${ + link.description ?? "No Description" + }` + ) + .join("\n")}`, + }, + ], + tools: [listingTool], + tool_choice: "required", + }); + + // Type guard to ensure it's a ChatCompletion (not a stream) + if (!("choices" in response)) { + // Unexpected response type + } + + const chatCompletion = response as ChatCompletion; + + // Check if the response contains tool calls + const message = chatCompletion.choices[0]?.message; + + if (message?.tool_calls && message.tool_calls.length > 0) { + // Prepare tool messages with function results + const toolCall = message.tool_calls[0]; + + // Type guard to check if it's a function tool call + if (toolCall.type === "function" && "function" in toolCall) { + const parsedArgs = JSON.parse(toolCall.function.arguments); + if (companyDirsCollected.length > 0) { + companyDirsCollected.push(...(parsedArgs.companyDirectories || [])); + } else { + companyDirsCollected.push(companyDirToSearch); + } + if (jobBoardsCollected.length > 0) { + jobBoardsCollected.push(...(parsedArgs.jobBoards || [])); + } + } + } + } + return { + companyDirsCollectedByMap: companyDirsCollected, + jobBoardsCollectedByMap: jobBoardsCollected, + }; +}; diff --git a/backend/scraper-cron/scrape-helpers/scrapeCompanyDirs.ts b/backend/scraper-cron/scrape-helpers/scrapeCompanyDirs.ts new file mode 100644 index 0000000..3a9fb53 --- /dev/null +++ b/backend/scraper-cron/scrape-helpers/scrapeCompanyDirs.ts @@ -0,0 +1,56 @@ +import { scrapeAndExtract } from "customScrapeAndExtract"; + +export const scrapeCompanyDirs = async (companyDirsToSearch: string[]) => { + const companyDirsCollected = []; + const jobBoardsCollected = []; + for (const dirUrl of companyDirsToSearch) { + // use custom extractor to get job boards, directories and companies + const result = await scrapeAndExtract({ url: dirUrl }); + + const { jobBoards, companyDirectories, companies } = result; + + companyDirsCollected.push(...companyDirectories.map((elem) => elem.url)); + jobBoardsCollected.push(...jobBoards.map((elem) => elem.url)); + + // for each company validate potential job boards. + for (const company of companies) { + if (!company.isStartup) break; + const buildPotentialJobBoardUrls = (url: string) => { + const baseUrlDomain = new URL(url).hostname; + const baseUrlDomainWithoutTld = new URL(url).hostname.replace( + /\.[^/.]+$/, + "" + ); + return [ + `https://${baseUrlDomain}/en-ca/careers`, + `https://${baseUrlDomain}/en-ca/jobs`, + `https://${baseUrlDomain}/careers`, + `https://${baseUrlDomain}/jobs`, + `https://jobs.lever.co/${baseUrlDomainWithoutTld}`, + `https://jobs.ashbyhq.com/${baseUrlDomainWithoutTld}`, + `https://${baseUrlDomainWithoutTld}.applytojob.com`, + ]; + }; + const potentialJobBoardUrls = buildPotentialJobBoardUrls(company.url); + for (const url of potentialJobBoardUrls) { + console.log("Checking job boards"); + try { + const res = await fetch(url, { + method: "HEAD", + signal: AbortSignal.timeout(1000), + }); + + if (res.ok) { + jobBoardsCollected.push(res.url); + break; + } + } catch (err) {} + } + } + } + + return { + companyDirsCollectedByScrape: companyDirsCollected, + jobBoardsCollectedByScrape: jobBoardsCollected, + }; +}; diff --git a/backend/scraper-cron/scrape-helpers/scrapeJobsFromJobBoards.ts b/backend/scraper-cron/scrape-helpers/scrapeJobsFromJobBoards.ts new file mode 100644 index 0000000..04e3b48 --- /dev/null +++ b/backend/scraper-cron/scrape-helpers/scrapeJobsFromJobBoards.ts @@ -0,0 +1,28 @@ +import { firecrawl, jobSchema } from "firecrawl"; + +export const scrapeJobsFromJobBoards = async (jobBoard: string) => { + try { + const result = await firecrawl.scrape(jobBoard, { + formats: [ + { + type: "json", + schema: jobSchema, + prompt: + "If this is actually a job board with jobs on it for a startup company, return the jobs in the json format, otherwise return an empty array.", + }, + ], + }); + + // Firecrawl returns result with json property containing the parsed schema + if (result.json && typeof result.json === "object" && "jobs" in result.json) { + const jobs = (result.json as { jobs: any[] }).jobs; + // Return the jobs array directly, ensuring it's an array + return Array.isArray(jobs) ? jobs : []; + } + + return []; + } catch (error) { + // If scraping fails, return empty array + return []; + } +}; diff --git a/backend/scraper-cron/sources.ts b/backend/scraper-cron/sources.ts new file mode 100644 index 0000000..6a98d19 --- /dev/null +++ b/backend/scraper-cron/sources.ts @@ -0,0 +1,169 @@ +// Places you’d actually hit for *jobs* (ATS + careers URLs) +export const jobBoardUrls = [ + // Candidate management systems (ATS) + // "https://jobs.ashbyhq.com/", + //"https://.applytojob.com", + "https://jobs.lever.co/", + + // Generic company careers paths + "https:///careers", + // "https:///jobs", + // "https:///en-ca/careers", + // "https:///en-ca/jobs" +]; + +// Directories, portfolios, ecosystems, accelerator/VC company lists, etc. +export const companyDirectoryUrls = [ + // To Contact / Ecosystem + // "https://www.startupblink.com/startup-ecosystem/canada", + + // Master Lists + "https://www.bycanada.tech/", +/* + "https://cswaccelerator.com/cohort-2-2/", + "https://cswaccelerator.com/cohort1/", + "https://www.ecommercenorth.ca/", + "https://www.voltaeffect.com/", + "https://www.foundersbeta.com/startup-directory/categories/edmonton-startup-accelerators-incubators/", + "https://www.foundersbeta.com/startup-directory/categories/guelph-startup-accelerators-incubators/", + "https://foresightcac.com/ventures/", + "https://www.brampton.ca/EN/Business/BEC/Pages/Welcome.aspx", + + "https://www.canstarthub.ca/discover", + "https://canadastartups.co/", + "https://growthlist.co/canada-startups/", + + // Smaller Programs Lists – GDrive lists + "https://drive.google.com/file/d/1Nx-0lyb_A24L6fJcHQm3AEnR_JFPE0OH/view", + "https://drive.google.com/file/d/1lRe_ZtmlYzt89g_RAViWaBvFZG6tEJSU/view?pli=1", + "https://drive.google.com/file/d/110rHwUA_I0q4uUevgF46FBhyQ1t3uYuV/view", + "https://drive.google.com/file/d/1JARpTeCVg4qjEhWMLUn8yhf9a8bQNMMn/view?usp=share_link", + + // Smaller Programs Lists – public directories / portfolios + "https://innovationfactory.ca/clients/", + "https://directory.nextcanada.com/directory/ventures/", + "https://startup.google.com/alumni/directory/?_gl=1*utxxg3*_up*MQ..*_ga*NTE2NDcyNDU3LjE3NjMwMTY1OTk.*_ga_GCB35PQ9X3*czE3NjMwMTY1OTgkbzEkZzAkdDE3NjMwMTY2MTQkajQ0JGwwJGgw®ion=canada", + "https://velocityincubator.com/companies/", + "https://www.intuit.com/ca/prosperity-accelerator/alumni/", + "https://www.safexconnected.com/cohorts", + "https://oneeleven-ca.squarespace.com/alumni", + "https://oneeleven-ca.squarespace.com/members", + "https://dmz.torontomu.ca/startup-directory", + "https://500.co/portfolio", + "https://www1.communitech.ca/companies", + "https://www.mcgill.ca/dobson/ourstartups", + "https://www.ualberta.ca/en/medicine/research/health-innovation-hub/companies.html", + "https://www.launchacademy.ca/alumni/", + "https://creativedestructionlab.com/companies/", + "https://www.founderfuel.com/companies/", + "https://www.yorku.ca/yspace/startups/", + "https://www.antler.co/portfolio", + "https://entrepreneurs.utoronto.ca/our-startups/startups-directory/", + "https://innovation.ubc.ca/entrepreneurship-ventures/portfolio-companies", + "https://www.acceleratorcentre.com/our-startups", + "https://www.l-spark.com/meet-our-companies/", + "https://airtable.com/app1Z7w2CdfNJt9rB/shrM8IADuuZf3wvOU/tblxzzU3yUX1ru667", + "https://hatchery.engineering.utoronto.ca/our-startup-teams/", + "https://www.tandemlaunch.com/portfolio", + "https://www.queensu.ca/innovationcentre/community/meet-startups", + "https://www.investottawa.ca/io-accelerator-companies/", + "https://entrepreneurship.uwo.ca/accelerator/our-venture-directory/", + "https://platformcalgary.webflow.io/community?tab=partners", + "https://icics.ubc.ca/hatch/alumni-hatch-ventures/", + "https://airtable.com/appurLNyQbHbSsCbJ/shrsOFwAHmVudJvsG/tbllsMtZKORBUYbXc", + "https://www.launchlab.ca/client-stories", + "https://edmontonunlimited.com/alumni-companies/", + "https://h2i.utoronto.ca/ventures/", + "https://foresightcac.com/companies", + "https://www.citm.ca/clients/", + "https://www.torontomu.ca/zone-learning/legal-innovation-zone/startups/", + "https://www.torontomu.ca/zone-learning/legal-innovation-zone/startups/#!tab-1745414258423-alumni-startups", + "https://www.torontomu.ca/zone-learning/biomedical-zone/startups1/", + "https://jnjinnovation.com/JLABSNavigator/", + "https://venturelabs.ca/companies/", + "https://www.ualberta.ca/en/business/alumni/business-directory.html", + "https://spinup.utm.utoronto.ca/our-startups/", + "https://app.marsdd.com/directory", + "https://mtlab.ca/startups/", + "https://lassonde.yorku.ca/best/startups/", + "https://www.district3.co/stories", + "https://www.georgebrown.ca/startgbc/alumni-entrepreneurs", + "https://www.plugandplaytechcenter.com/venture-capital/startup-portfolio", + "https://www.torontomu.ca/zone-learning/sdz/startups/", + "https://www.torontomu.ca/zone-learning/fashion-zone/companies/", + "https://holtxchange.com/portfolio/", + "https://www.stationfintech.com/en/our-startups", + "https://www.co-labs.ca/our-startups", + "https://www.loi.vc/portfolio/", + "https://spring.is/impact-capital/portfolio/", + "https://www.torontomu.ca/svz/startups/", + + // VC / program portfolios & directories + "https://www.torontomu.ca/transmedia-zone/startups/", + "https://www.startupcalgary.ca/launch-party", + "https://www.newventuresbc.com/alumni", + "https://www.globalstartups.io/portfolio", + "https://www.bramptonventurezone.ca/companies", + "https://ideasinc.ca/companies/", + "https://foresightcac.com/ventures/", + "https://www.brampton.ca/EN/Business/BEC/Pages/Welcome.aspx", + "https://www.parkdaleinnovates.org/", + "https://canadastechnetwork.ca/members/", + "https://lazaridisinstitute.ca/scaleup/companies/", + "https://lazaridisinstitute.ca/scaleup/", + "https://www.torontomu.ca/clean-energy-zone/", + "https://www.halton.ca/For-Business/Starting-a-Business/Small-Business-Centre", + "https://centech.co/startups/", + "https://techalliance.ca/directory/", + "https://www.ceed.ca/", + "https://www.torontomu.ca/design-fabrication-zone/startups/", + "https://theforgehamilton.ca/companies/", + "https://www.torontomu.ca/innovation-boost-zone/startups/", + "https://lapiscine.co/", + "https://canadasmusicincubator.com/", + "https://www.centennialcollege.ca/centres-institutes/accel", + "https://www.artscape.ca/launchpad/", + "https://www.shad.ca/", + "https://www.tradecommissioner.gc.ca/cta-atc/index.aspx?lang=eng", + "https://www.hec.ca/en/entrepreneurs/la-base/", + "https://www.thec100.org/fellows", + "https://northforge.ca/clients/", + "https://altitudeaccelerator.ca/ventures/", + "https://www.torontomu.ca/cybersecure-catalyst/accelerator/companies/", + "https://www.bhive.ca/companies", + "https://techplace.ca/residents/", + "https://innovationfactory.ca/client-directory/", + "https://www.albertacatalyzer.com/", + "https://www.plugandplaytechcenter.com/alberta/", + "https://www.albertaiot.com/directory/", + "https://www.pacifictechnologyventures.com/", + "https://www.intrinsicinnovations.ca/", + "https://www.reachinsurtech.com/canada", + "https://www.platformcalgary.com/", + + // FoundersBeta startup-accelerator/ incubator directories + "https://www.foundersbeta.com/startup-directory/categories/edmonton-startup-accelerators-incubators/", + "https://www.foundersbeta.com/startup-directory/categories/guelph-startup-accelerators-incubators/", + "https://www.foundersbeta.com/startup-directory/categories/halifax-startup-accelerators-incubators/", + "https://www.foundersbeta.com/startup-directory/categories/hamilton-startup-accelerators-incubators/", + "https://www.foundersbeta.com/startup-directory/categories/kingston-startup-accelerators-incubators/", + "https://www.foundersbeta.com/startup-directory/categories/london-ontario-startup-accelerators-incubators/", + "https://www.foundersbeta.com/startup-directory/categories/markham-startup-accelerators-incubators/", + "https://www.foundersbeta.com/startup-directory/categories/mississauga-startup-accelerators-incubators/", + "https://www.foundersbeta.com/startup-directory/categories/montreal-startup-accelerators-incubators/", + "https://www.foundersbeta.com/startup-directory/categories/ottawa-startup-accelerators-incubators/", + "https://www.foundersbeta.com/startup-directory/categories/startup-accelerators-and-incubators/", + "https://www.foundersbeta.com/startup-directory/categories/toronto-startup-accelerators-incubators/", + "https://www.foundersbeta.com/startup-directory/categories/vancouver-startup-accelerators-incubators/", + "https://www.foundersbeta.com/startup-directory/categories/waterloo-startup-accelerators-incubators/", + + // Other Startup Resources (mostly ecosystems / directories / members) + "https://www.ecommercenorth.ca/", + "https://www.voltaeffect.com/", + "https://albertainnovates.ca/", + "https://www.theksociety.com/", + "https://cybersecurecatalyst.ca/catalyst-cyber-accelerator/", + "https://bioenterprise.ca/member-directory/", + "https://tbdc.com/", + "https://www.theforum.ca/", +*/]; diff --git a/backend/scraper-cron/test-bullmq.ts b/backend/scraper-cron/test-bullmq.ts new file mode 100644 index 0000000..009afdf --- /dev/null +++ b/backend/scraper-cron/test-bullmq.ts @@ -0,0 +1,37 @@ +// Test script to verify BullMQ is working with Redis +import { companyDirectoryQueue, jobBoardQueue, closeAllQueues } from "./queues"; +import { connectRedis } from "./redisClient"; +import dotenv from "dotenv"; + +dotenv.config(); + +const testBullMQ = async () => { + try { + // Connect to Redis + await connectRedis(); + + // Test adding jobs to queues + const companyDirJob = await companyDirectoryQueue.add("scrape-directory", { + url: "https://www.bycanada.tech/", + timestamp: Date.now(), + }); + + const jobBoardJob = await jobBoardQueue.add("scrape-job-board", { + url: "https://example.com/careers", + timestamp: Date.now(), + }); + + // Check queue status + const companyDirCount = await companyDirectoryQueue.getWaitingCount(); + const jobBoardCount = await jobBoardQueue.getWaitingCount(); + + await closeAllQueues(); + process.exit(0); + } catch (error) { + await closeAllQueues(); + process.exit(1); + } +}; + +testBullMQ(); + diff --git a/backend/scraper-cron/tsconfig.json b/backend/scraper-cron/tsconfig.json new file mode 100644 index 0000000..2ea5fc5 --- /dev/null +++ b/backend/scraper-cron/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "target": "ES6", + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "incremental": true, + "baseUrl": ".", + "paths": { + "@/*": [ + "src/*" + ] + } + }, + "include": [ + "**/*.ts", + ], + "exclude": [ + "node_modules" + ] +} diff --git a/backend/scraper-cron/utils.ts b/backend/scraper-cron/utils.ts new file mode 100644 index 0000000..5a22879 --- /dev/null +++ b/backend/scraper-cron/utils.ts @@ -0,0 +1,16 @@ +export const dedupeArray = (arr: string[]): string[] => { + return Array.from(new Set(arr)); +}; + +export const chunkStrings = ( + input: string[], + chunkLength: number +): string[][] => { + const result: string[][] = []; + + for (let i = 0; i < input.length; i += chunkLength) { + result.push(input.slice(i, i + chunkLength)); + } + + return result; +}; diff --git a/backend/scraper-cron/workers/companyDirWorker.ts b/backend/scraper-cron/workers/companyDirWorker.ts new file mode 100644 index 0000000..e3016d9 --- /dev/null +++ b/backend/scraper-cron/workers/companyDirWorker.ts @@ -0,0 +1,59 @@ +import { companyDirectoryQueue, jobBoardQueue, redisConnection } from "queues"; +import { scrapeCompanyDirs } from "scrape-helpers/scrapeCompanyDirs"; +import { Worker } from "bullmq"; +import { WORKER_CONCURRENCY } from "../constants"; + +export const companyDirectoryWorker = new Worker( + "company-directories", + async (job) => { + const depth = job.data.depth ?? 0; + try { + console.log("Scraping company dir", job.data.url); + // Scrape the company directory to discover job boards and more directories + const { companyDirsCollectedByScrape, jobBoardsCollectedByScrape } = + await scrapeCompanyDirs([job.data.url]); + console.log( + "Job boards from scrape", + jobBoardsCollectedByScrape, + job.data.url + ); + // Add discovered job boards to the job board queue + const jobBoardPromises = jobBoardsCollectedByScrape.map( + (jobBoardUrl, index) => + index < WORKER_CONCURRENCY.JOB_BOARD_BREADTH + ? jobBoardQueue.add("scrape-job-board", { url: jobBoardUrl }) + : Promise.resolve() + ); + await Promise.allSettled(jobBoardPromises); + + // Recurse once: if depth is 0, add discovered directories with depth 1 + // If depth is already 1, don't recurse further + if (depth === 0 && companyDirsCollectedByScrape.length > 0) { + const dirPromises = companyDirsCollectedByScrape.map((dirUrl, index) => + index < WORKER_CONCURRENCY.COMPANY_DIRECTORY_BREADTH + ? companyDirectoryQueue.add("scrape-company-directory", { + url: dirUrl, + depth: 1, + }) + : Promise.resolve() + ); + await Promise.allSettled(dirPromises); + } + + return { + success: true, + url: job.data.url, + depth, + jobBoardsFound: jobBoardsCollectedByScrape.length, + directoriesFound: companyDirsCollectedByScrape.length, + recursed: depth === 0 && companyDirsCollectedByScrape.length > 0, + }; + } catch (error) { + throw error; + } + }, + { + connection: redisConnection, + concurrency: WORKER_CONCURRENCY.COMPANY_DIRECTORY, + } +); diff --git a/backend/scraper-cron/workers/jobBoardWorker.ts b/backend/scraper-cron/workers/jobBoardWorker.ts new file mode 100644 index 0000000..16901e4 --- /dev/null +++ b/backend/scraper-cron/workers/jobBoardWorker.ts @@ -0,0 +1,142 @@ +import { scrapeJobsFromJobBoards } from "scrape-helpers/scrapeJobsFromJobBoards"; +import { Worker } from "bullmq"; +import { redisConnection } from "../queues"; +import { db, jobs } from "@canadian-startup-jobs/db"; +import { eq } from "drizzle-orm"; +import { + WORKER_CONCURRENCY, + RATE_LIMITER, +} from "../constants"; + +// Type for scraped job data +type ScrapedJob = { + title: string; + location: string; + remoteOk?: boolean; + salaryMin?: number; + salaryMax?: number; + description: string; + company: string; + jobBoardUrl?: string; + postingUrl?: string; + isAtAStartup?: boolean; +}; + +export const jobBoardWorker = new Worker( + "job-boards", + async (job) => { + try { + // Scrape one job board at a time + const scrapedJobs = (await scrapeJobsFromJobBoards( + job.data.url, + )) as ScrapedJob[]; + + if (scrapedJobs.length === 0) { + return { success: true, url: job.data.url, jobsInserted: 0 }; + } + + // Transform scraped jobs to match database schema + const jobsToUpsert = scrapedJobs.map((jobData) => ({ + title: jobData.title, + location: jobData.location, + remoteOk: jobData.remoteOk ?? false, + salaryMin: jobData.salaryMin ?? null, + salaryMax: jobData.salaryMax ?? null, + description: jobData.description, + company: jobData.company, + jobBoardUrl: jobData.jobBoardUrl ?? job.data.url, + postingUrl: jobData.postingUrl ?? null, + isAtAStartup: jobData.isAtAStartup ?? null, + })); + + // Bulk upsert jobs using postingUrl as unique identifier + // Separate jobs with and without postingUrl + const jobsWithUrl = jobsToUpsert.filter((j) => j.postingUrl); + const jobsWithoutUrl = jobsToUpsert.filter((j) => !j.postingUrl); + + let insertedCount = 0; + let updatedCount = 0; + + // Handle jobs with postingUrl (check for existing) + if (jobsWithUrl.length > 0) { + // Check each job individually to see if it exists + // Using individual queries to work around drizzle-orm version type conflicts + const existingUrls = new Set(); + + for (const jobData of jobsWithUrl) { + if (!jobData.postingUrl) continue; + + try { + // Use Drizzle ORM to check if job exists + // Type assertion needed due to drizzle-orm version mismatch between packages + const existing = await db + .select({ postingUrl: jobs.postingUrl }) + .from(jobs) + // @ts-expect-error - Type mismatch between drizzle-orm versions, but works at runtime + .where(eq(jobs.postingUrl, jobData.postingUrl)) + .limit(1); + + if (existing.length > 0 && existing[0].postingUrl) { + existingUrls.add(existing[0].postingUrl); + } + } catch (error) { + // If query fails, assume job doesn't exist and will be inserted + } + } + + const toInsert = jobsWithUrl.filter( + (j) => j.postingUrl && !existingUrls.has(j.postingUrl) + ); + const toUpdate = jobsWithUrl.filter( + (j) => j.postingUrl && existingUrls.has(j.postingUrl) + ); + + // Batch insert new jobs + if (toInsert.length > 0) { + await db.insert(jobs).values(toInsert); + insertedCount += toInsert.length; + } + + // Update existing jobs one by one using Drizzle ORM + for (const jobData of toUpdate) { + if (!jobData.postingUrl) continue; + + try { + await db + .update(jobs) + .set({ + ...jobData, + updatedAt: new Date(), + }) + // @ts-expect-error - Type mismatch between drizzle-orm versions, but works at runtime + .where(eq(jobs.postingUrl, jobData.postingUrl)); + updatedCount++; + } catch (error) { + // Error updating job + } + } + } + + // Insert jobs without postingUrl (always new) + if (jobsWithoutUrl.length > 0) { + await db.insert(jobs).values(jobsWithoutUrl); + insertedCount += jobsWithoutUrl.length; + } + + return { + success: true, + url: job.data.url, + jobsInserted: insertedCount, + jobsUpdated: updatedCount, + totalJobs: scrapedJobs.length, + }; + } catch (error) { + throw error; + } + }, + { + connection: redisConnection, + concurrency: WORKER_CONCURRENCY.JOB_BOARD, + limiter: RATE_LIMITER.FIRECRAWL, + } +); diff --git a/backend/scraper-cron/workers/mapCompanyDirWorker.ts b/backend/scraper-cron/workers/mapCompanyDirWorker.ts new file mode 100644 index 0000000..d46a387 --- /dev/null +++ b/backend/scraper-cron/workers/mapCompanyDirWorker.ts @@ -0,0 +1,49 @@ +import { Worker } from "bullmq"; +import { mapCompanyDir } from "../scrape-helpers/mapCompanyDirs"; +import { companyDirectoryQueue, jobBoardQueue } from "../queues"; +import { redisConnection } from "../queues"; +import { WORKER_CONCURRENCY } from "../constants"; + +export const mapCompanyDirWorker = new Worker( + "map-company-directories", + async (job) => { + try { + // Call mapCompanyDir with a single URL array + const { companyDirsCollectedByMap, jobBoardsCollectedByMap } = + await mapCompanyDir([job.data.url]); + console.log("Job boards from map", jobBoardsCollectedByMap, job.data.url); + // Add discovered company directories to the scrape queue (depth 0) + const dirPromises = companyDirsCollectedByMap.map((dirUrl, index) => + index < WORKER_CONCURRENCY.COMPANY_DIRECTORY_BREADTH + ? companyDirectoryQueue.add("scrape-company-directory", { + url: dirUrl, + depth: 0, + }) + : Promise.resolve() + ); + await Promise.allSettled(dirPromises); + + // Add discovered job boards to the job board queue + const jobBoardPromises = jobBoardsCollectedByMap.map( + (jobBoardUrl, index) => + index < WORKER_CONCURRENCY.JOB_BOARD_BREADTH + ? jobBoardQueue.add("scrape-job-board", { url: jobBoardUrl }) + : Promise.resolve() + ); + await Promise.allSettled(jobBoardPromises); + + return { + success: true, + url: job.data.url, + directoriesFound: companyDirsCollectedByMap.length, + jobBoardsFound: jobBoardsCollectedByMap.length, + }; + } catch (error) { + throw error; + } + }, + { + connection: redisConnection, + concurrency: WORKER_CONCURRENCY.MAP_COMPANY_DIR, + } +); diff --git a/backend/scraper-cron/workers/workers.ts b/backend/scraper-cron/workers/workers.ts new file mode 100644 index 0000000..8e574ad --- /dev/null +++ b/backend/scraper-cron/workers/workers.ts @@ -0,0 +1,63 @@ +import { Worker } from "bullmq"; +import dotenv from "dotenv"; +import { connectRedis } from "../redisClient"; +import { jobBoardWorker } from "./jobBoardWorker"; +import { companyDirectoryWorker } from "./companyDirWorker"; +import { mapCompanyDirWorker } from "./mapCompanyDirWorker"; + +dotenv.config(); + + + +// Set up event listeners +mapCompanyDirWorker.on("completed", (job) => { + // Job completed +}); + +mapCompanyDirWorker.on("failed", (job, err) => { + // Job failed +}); + +companyDirectoryWorker.on("completed", (job) => { + // Job completed +}); + +companyDirectoryWorker.on("failed", (job, err) => { + // Job failed +}); + +jobBoardWorker.on("completed", (job) => { + // Job completed +}); + +jobBoardWorker.on("failed", (job, err) => { + // Job failed +}); + +// Initialize Redis connection when workers start +mapCompanyDirWorker.on("ready", async () => { + await connectRedis(); +}); + +companyDirectoryWorker.on("ready", async () => { + // Worker ready +}); + +jobBoardWorker.on("ready", async () => { + // Worker ready +}); + + +// Graceful shutdown +const shutdown = async () => { + await Promise.all([ + mapCompanyDirWorker.close(), + companyDirectoryWorker.close(), + jobBoardWorker.close() + ]); + process.exit(0); +}; + +process.on("SIGTERM", shutdown); +process.on("SIGINT", shutdown); + diff --git a/db/.env.example b/db/.env.example new file mode 100644 index 0000000..154f0fa --- /dev/null +++ b/db/.env.example @@ -0,0 +1,10 @@ +# Database Configuration +# These values match the docker-compose.yml defaults +POSTGRES_HOST=localhost +POSTGRES_PORT=5432 +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres +POSTGRES_DB=canadian_startup_jobs + +# Alternative: Use a connection string instead +# DATABASE_URL=postgresql://postgres:postgres@localhost:5432/canadian_startup_jobs diff --git a/db/README.md b/db/README.md new file mode 100644 index 0000000..9e0cfa8 --- /dev/null +++ b/db/README.md @@ -0,0 +1,99 @@ +# @canadian-startup-jobs/db + + +Currently skeleton - feel free to throw this out and create a new one. +Database package for the Canadian Startup Jobs monorepo. This package provides a shared Drizzle ORM connection to PostgreSQL that can be used across multiple services. + +## Installation + +From other packages in the monorepo, you can import this package: + +```typescript +import { db, schema } from "@canadian-startup-jobs/db"; +``` + +## Setup + +1. Install dependencies: +```bash +npm install +``` + +2. Configure environment variables (copy `.env.example` to `.env` and adjust if needed): +```bash +cp .env.example .env +``` + +3. Build the package: +```bash +npm run build +``` + +## Usage + +### Basic Query Example + +```typescript +import { db } from "@canadian-startup-jobs/db"; +import { users } from "@canadian-startup-jobs/db/schema"; + +// Query example +const allUsers = await db.select().from(users); +``` + +### Using in Other Services + +In your service's `package.json`, add this package as a dependency: + +```json +{ + "dependencies": { + "@canadian-startup-jobs/db": "workspace:*" + } +} +``` + +Then import and use: + +```typescript +import { db } from "@canadian-startup-jobs/db"; +``` + +## Database Migrations + +Generate migrations: +```bash +npm run db:generate +``` + +Push schema changes directly (for development): +```bash +npm run db:push +``` + +Run migrations: +```bash +npm run db:migrate +``` + +Open Drizzle Studio (database GUI): +```bash +npm run db:studio +``` + +## Environment Variables + +The package uses the following environment variables (with defaults matching docker-compose.yml): + +- `POSTGRES_HOST` (default: `localhost`) +- `POSTGRES_PORT` (default: `5432`) +- `POSTGRES_USER` (default: `postgres`) +- `POSTGRES_PASSWORD` (default: `postgres`) +- `POSTGRES_DB` (default: `canadian_startup_jobs`) + +Alternatively, you can use a `DATABASE_URL` connection string. + +## Schema + +Define your database schema in `src/schema/index.ts` or create separate schema files and export them from the index. + diff --git a/db/drizzle.config.ts b/db/drizzle.config.ts new file mode 100644 index 0000000..38f386c --- /dev/null +++ b/db/drizzle.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from "drizzle-kit"; +import * as dotenv from "dotenv"; + +dotenv.config(); + +export default defineConfig({ + schema: "./src/schema/index.ts", + out: "./drizzle", + dialect: "postgresql", + dbCredentials: process.env.DATABASE_URL + ? { url: process.env.DATABASE_URL } + : { + host: process.env.POSTGRES_HOST || "localhost", + port: parseInt(process.env.POSTGRES_PORT || "5433"), + user: process.env.POSTGRES_USER || "postgres", + password: process.env.POSTGRES_PASSWORD || "postgres", + database: process.env.POSTGRES_DB || "canadian_startup_db", + }, +}); + diff --git a/db/package-lock.json b/db/package-lock.json new file mode 100644 index 0000000..440ba34 --- /dev/null +++ b/db/package-lock.json @@ -0,0 +1,1279 @@ +{ + "name": "@canadian-startup-jobs/db", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@canadian-startup-jobs/db", + "version": "0.1.0", + "dependencies": { + "drizzle-orm": "^0.36.4", + "postgres": "^3.4.5" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "dotenv": "^17.2.3", + "drizzle-kit": "^0.30.0", + "typescript": "^5" + }, + "peerDependencies": { + "dotenv": "^17.2.3" + } + }, + "node_modules/@drizzle-team/brocli": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz", + "integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@esbuild-kit/core-utils": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", + "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==", + "deprecated": "Merged into tsx: https://tsx.is", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.18.20", + "source-map-support": "^0.5.21" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/@esbuild-kit/esm-loader": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz", + "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==", + "deprecated": "Merged into tsx: https://tsx.is", + "dev": true, + "license": "MIT", + "dependencies": { + "@esbuild-kit/core-utils": "^3.3.2", + "get-tsconfig": "^4.7.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@petamoriken/float16": { + "version": "3.9.3", + "resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.9.3.tgz", + "integrity": "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/drizzle-kit": { + "version": "0.30.6", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.30.6.tgz", + "integrity": "sha512-U4wWit0fyZuGuP7iNmRleQyK2V8wCuv57vf5l3MnG4z4fzNTjY/U13M8owyQ5RavqvqxBifWORaR3wIUzlN64g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@drizzle-team/brocli": "^0.10.2", + "@esbuild-kit/esm-loader": "^2.5.5", + "esbuild": "^0.19.7", + "esbuild-register": "^3.5.0", + "gel": "^2.0.0" + }, + "bin": { + "drizzle-kit": "bin.cjs" + } + }, + "node_modules/drizzle-orm": { + "version": "0.36.4", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.36.4.tgz", + "integrity": "sha512-1OZY3PXD7BR00Gl61UUOFihslDldfH4NFRH2MbP54Yxi0G/PKn4HfO65JYZ7c16DeP3SpM3Aw+VXVG9j6CRSXA==", + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=3", + "@electric-sql/pglite": ">=0.2.0", + "@libsql/client": ">=0.10.0", + "@libsql/client-wasm": ">=0.10.0", + "@neondatabase/serverless": ">=0.10.0", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/react": ">=18", + "@types/sql.js": "*", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=14.0.0", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "react": ">=18", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@libsql/client-wasm": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/react": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "react": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, + "node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "node_modules/esbuild-register": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz", + "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "peerDependencies": { + "esbuild": ">=0.12 <1" + } + }, + "node_modules/gel": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/gel/-/gel-2.2.0.tgz", + "integrity": "sha512-q0ma7z2swmoamHQusey8ayo8+ilVdzDt4WTxSPzq/yRqvucWRfymRVMvNgmSC0XK7eNjjEZEcplxpgaNojKdmQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@petamoriken/float16": "^3.8.7", + "debug": "^4.3.4", + "env-paths": "^3.0.0", + "semver": "^7.6.2", + "shell-quote": "^1.8.1", + "which": "^4.0.0" + }, + "bin": { + "gel": "dist/cli.mjs" + }, + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/postgres": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", + "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==", + "license": "Unlicense", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/porsager" + } + }, + "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/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + } + } +} diff --git a/db/package.json b/db/package.json new file mode 100644 index 0000000..e332e42 --- /dev/null +++ b/db/package.json @@ -0,0 +1,37 @@ +{ + "name": "@canadian-startup-jobs/db", + "version": "0.1.0", + "private": true, + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "db:generate": "drizzle-kit generate", + "db:migrate": "drizzle-kit migrate", + "db:push": "drizzle-kit push", + "db:studio": "drizzle-kit studio" + }, + "dependencies": { + "drizzle-orm": "^0.36.4", + "postgres": "^3.4.5" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "drizzle-kit": "^0.30.0", + "dotenv": "^17.2.3", + "typescript": "^5" + }, + "peerDependencies": { + "dotenv": "^17.2.3" + } +} + diff --git a/db/src/index.ts b/db/src/index.ts new file mode 100644 index 0000000..0a945b4 --- /dev/null +++ b/db/src/index.ts @@ -0,0 +1,25 @@ +import { drizzle } from "drizzle-orm/postgres-js"; +import postgres from "postgres"; +import * as schema from "./schema/index.js"; + +// Get database connection string from environment variables +const connectionString = process.env.DATABASE_URL || + `postgresql://${process.env.POSTGRES_USER || "postgres"}:${process.env.POSTGRES_PASSWORD || "postgres"}@${process.env.POSTGRES_HOST || "localhost"}:${process.env.POSTGRES_PORT || "5433"}/${process.env.POSTGRES_DB || "canadian_startup_db"}`; + +// Create postgres client +const client = postgres(connectionString, { + max: 10, + idle_timeout: 20, + connect_timeout: 10, +}); + +// Create drizzle instance +export const db = drizzle(client, { schema }); + +// Export schema for use in other services +export * from "./schema/index.js"; +export { schema }; + +// Export types +export type Database = typeof db; + diff --git a/db/src/schema/index.ts b/db/src/schema/index.ts new file mode 100644 index 0000000..7438b29 --- /dev/null +++ b/db/src/schema/index.ts @@ -0,0 +1,28 @@ +// Export all schema definitions from this file +// Add your table schemas here or import them from separate files + +import { + pgTable, + serial, + text, + boolean, + integer, + timestamp, +} from "drizzle-orm/pg-core"; + +export const jobs = pgTable("jobs", { + id: serial("id").primaryKey(), + title: text("title").notNull(), + location: text("location").notNull(), + remoteOk: boolean("remote_ok").notNull(), + salaryMin: integer("salary_min"), + salaryMax: integer("salary_max"), + description: text("description").notNull(), + company: text("company").notNull(), + jobBoardUrl: text("job_board_url"), + postingUrl: text("posting_url"), + isAtAStartup: boolean("is_at_a_startup"), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}); + diff --git a/db/tsconfig.json b/db/tsconfig.json new file mode 100644 index 0000000..eb3098c --- /dev/null +++ b/db/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "lib": ["ES2022"], + "moduleResolution": "bundler", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "isolatedModules": true, + "noEmit": false + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..0c15b98 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,41 @@ +version: '3.8' + +services: + postgres: + image: postgres:16 + container_name: postgres_local + restart: always + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: canadian_startup_db + ports: + - "5433:5432" + volumes: + - pgdata:/var/lib/postgresql/data + + redis: + image: redis:7-alpine + container_name: canadian-startup-jobs-redis + restart: unless-stopped + ports: + - "${REDIS_PORT:-6379}:6379" + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - canadian-startup-jobs-network + +volumes: + pgdata: + redis_data: + driver: local + +networks: + canadian-startup-jobs-network: + driver: bridge + diff --git a/test-db-connection.ps1 b/test-db-connection.ps1 new file mode 100644 index 0000000..3d70229 --- /dev/null +++ b/test-db-connection.ps1 @@ -0,0 +1,34 @@ +# Test PostgreSQL Connection Script +Write-Host "Testing PostgreSQL connection..." -ForegroundColor Cyan + +# Test 1: Check if port is accessible +Write-Host "`n1. Testing port accessibility..." -ForegroundColor Yellow +$portTest = Test-NetConnection -ComputerName localhost -Port 5432 -InformationLevel Quiet +if ($portTest) { + Write-Host " Port 5432 is accessible" -ForegroundColor Green +} else { + Write-Host " Port 5432 is NOT accessible" -ForegroundColor Red + exit 1 +} + +# Test 2: Test connection using docker exec +Write-Host "`n2. Testing connection from inside container..." -ForegroundColor Yellow +$testResult = docker exec canadian-startup-jobs-db sh -c "PGPASSWORD=password psql -h 127.0.0.1 -U postgres -d postgres -c 'SELECT version();' 2>&1" +if ($LASTEXITCODE -eq 0) { + Write-Host " Connection from inside container works" -ForegroundColor Green + $version = $testResult | Select-String 'PostgreSQL' + Write-Host " Database version: $version" -ForegroundColor Gray +} else { + Write-Host " Connection failed" -ForegroundColor Red + Write-Host " Error: $testResult" -ForegroundColor Red +} + +# Display connection information +Write-Host "`n3. Connection Information for pgAdmin:" -ForegroundColor Yellow +Write-Host " Host: 127.0.0.1" -ForegroundColor White +Write-Host " Port: 5432" -ForegroundColor White +Write-Host " Maintenance DB: postgres" -ForegroundColor White +Write-Host " Username: postgres" -ForegroundColor White +Write-Host " Password: password" -ForegroundColor White + +Write-Host "`nAll tests completed!" -ForegroundColor Green