From 1a9437030e86202bf68cfeb760678e441fab4628 Mon Sep 17 00:00:00 2001 From: Jake Jarvis Date: Fri, 17 Oct 2025 23:50:29 -0400 Subject: [PATCH] v2: Postgres storage & Inngest background revalidations (#101) --- .github/workflows/test.yml | 2 +- README.md | 37 + app/api/inngest/route.ts | 13 + biome.json | 2 +- components/domain/export-data.test.tsx | 1 - drizzle.config.ts | 10 + drizzle/0000_nosy_wendell_rand.sql | 154 + drizzle/meta/0000_snapshot.json | 1207 ++++++++ drizzle/meta/_journal.json | 13 + lib/schemas/domain/registration.ts | 1 - lib/schemas/index.ts | 1 + lib/schemas/internal/sections.ts | 12 + package.json | 27 +- pnpm-lock.yaml | 2662 +++++++++++++++-- pnpm-workspace.yaml | 1 + server/db/client.ts | 12 + server/db/pglite.ts | 20 + server/db/schema.ts | 267 ++ server/db/seed/providers.ts | 61 + server/db/ttl.test.ts | 50 + server/db/ttl.ts | 55 + server/inngest/client.ts | 4 + server/inngest/functions/domain-inspected.ts | 29 + server/inngest/functions/scan-due.test.ts | 17 + server/inngest/functions/scan-due.ts | 147 + .../functions/section-revalidate.test.ts | 111 + .../inngest/functions/section-revalidate.ts | 94 + server/repos/certificates.ts | 39 + server/repos/dns.ts | 98 + server/repos/domains.ts | 37 + server/repos/headers.ts | 45 + server/repos/hosting.ts | 13 + server/repos/providers.ts | 46 + server/repos/registrations.ts | 72 + server/repos/seo.ts | 13 + server/routers/domain.ts | 11 +- server/services/certificates.test.ts | 77 +- server/services/certificates.ts | 109 +- server/services/dns.test.ts | 112 +- server/services/dns.ts | 456 +-- server/services/headers.test.ts | 76 +- server/services/headers.ts | 132 +- server/services/hosting.test.ts | 41 +- server/services/hosting.ts | 159 +- server/services/pricing.ts | 6 +- server/services/registration.test.ts | 119 +- server/services/registration.ts | 163 +- server/services/seo.test.ts | 78 +- server/services/seo.ts | 193 +- vitest.setup.ts | 190 +- 50 files changed, 6373 insertions(+), 922 deletions(-) create mode 100644 app/api/inngest/route.ts create mode 100644 drizzle.config.ts create mode 100644 drizzle/0000_nosy_wendell_rand.sql create mode 100644 drizzle/meta/0000_snapshot.json create mode 100644 drizzle/meta/_journal.json create mode 100644 lib/schemas/internal/sections.ts create mode 100644 server/db/client.ts create mode 100644 server/db/pglite.ts create mode 100644 server/db/schema.ts create mode 100644 server/db/seed/providers.ts create mode 100644 server/db/ttl.test.ts create mode 100644 server/db/ttl.ts create mode 100644 server/inngest/client.ts create mode 100644 server/inngest/functions/domain-inspected.ts create mode 100644 server/inngest/functions/scan-due.test.ts create mode 100644 server/inngest/functions/scan-due.ts create mode 100644 server/inngest/functions/section-revalidate.test.ts create mode 100644 server/inngest/functions/section-revalidate.ts create mode 100644 server/repos/certificates.ts create mode 100644 server/repos/dns.ts create mode 100644 server/repos/domains.ts create mode 100644 server/repos/headers.ts create mode 100644 server/repos/hosting.ts create mode 100644 server/repos/providers.ts create mode 100644 server/repos/registrations.ts create mode 100644 server/repos/seo.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5e4f2c1..525d59a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,7 +23,7 @@ jobs: uses: pnpm/action-setup@v4 - name: Setup Node.js - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: "22" cache: "pnpm" diff --git a/README.md b/README.md index 30cb56c..082f9d6 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,43 @@ --- +## 🔄 v2 Cutover: Postgres + Drizzle + Inngest + +- **Primary store**: Postgres (Neon). All domain sections persist to tables in `server/db/schema.ts` via Drizzle. +- **Drizzle**: Schema/migrations in `drizzle/`. Config in `drizzle.config.ts`. Client at `server/db/client.ts`. +- **Redis role**: Short-lived locks, rate limiting, and image/report caches only (no primary data). See `lib/cache.ts`, `lib/report-cache.ts`. +- **Background jobs (Inngest)**: + - `app/api/inngest/route.ts` serves functions. + - `section-revalidate`: re-fetch a section for a domain. + - `domain-inspected`: fans out per-section revalidation. + - `scan-due`: cron to enqueue revalidations for stale rows. +- **TTL & freshness**: Policies in `server/db/ttl.ts`. Each service reads from Postgres first and revalidates when stale. +- **Services**: `server/services/*` now read/write Postgres via repos in `server/repos/*`. + +### Environment +- `DATABASE_URL` (required) +- Redis/UploadThing/PostHog remain as before (see `.env.example`). + +### Commands +```bash +# Drizzle +pnpm drizzle:generate +pnpm drizzle:migrate + +# Dev / checks / tests +pnpm dev +pnpm lint +pnpm typecheck +pnpm test:run +``` + +### Notes +- Provider catalog is seeded from `lib/providers/rules/*` via `server/db/seed/providers.ts`. +- Trigram search enabled via `pg_trgm` migration in `drizzle/`. +- No back-compat/migration from Redis snapshots; v2 is a clean switch. + +--- + ## 📜 License [MIT](LICENSE) diff --git a/app/api/inngest/route.ts b/app/api/inngest/route.ts new file mode 100644 index 0000000..610e6eb --- /dev/null +++ b/app/api/inngest/route.ts @@ -0,0 +1,13 @@ +import { serve } from "inngest/next"; +import { inngest } from "@/server/inngest/client"; +import { domainInspected } from "@/server/inngest/functions/domain-inspected"; +import { scanDue } from "@/server/inngest/functions/scan-due"; +import { sectionRevalidate } from "@/server/inngest/functions/section-revalidate"; + +// opt out of caching per Inngest docs +export const dynamic = "force-dynamic"; + +export const { GET, POST, PUT } = serve({ + client: inngest, + functions: [sectionRevalidate, domainInspected, scanDue], +}); diff --git a/biome.json b/biome.json index 6c175b4..da0ae73 100644 --- a/biome.json +++ b/biome.json @@ -7,7 +7,7 @@ }, "files": { "ignoreUnknown": true, - "includes": ["**", "!node_modules", "!.next", "!dist", "!build"] + "includes": ["**", "!node_modules", "!.next", "!dist", "!build", "!drizzle"] }, "formatter": { "enabled": true, diff --git a/components/domain/export-data.test.tsx b/components/domain/export-data.test.tsx index 3bec166..164aee8 100644 --- a/components/domain/export-data.test.tsx +++ b/components/domain/export-data.test.tsx @@ -42,7 +42,6 @@ describe("exportDomainData", () => { registrar: { name: "Test Registrar" }, warnings: [], unicodeName: "example.com", - punycodeName: "example.com", registrarProvider: { name: "Test Registrar", domain: "testregistrar.com", diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..1d3f341 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,10 @@ +import type { Config } from "drizzle-kit"; + +export default { + schema: "./server/db/schema.ts", + out: "./drizzle", + dialect: "postgresql", + dbCredentials: { + url: process.env.DATABASE_URL as string, + }, +} satisfies Config; diff --git a/drizzle/0000_nosy_wendell_rand.sql b/drizzle/0000_nosy_wendell_rand.sql new file mode 100644 index 0000000..611b4cb --- /dev/null +++ b/drizzle/0000_nosy_wendell_rand.sql @@ -0,0 +1,154 @@ +CREATE TYPE "public"."dns_record_type" AS ENUM('A', 'AAAA', 'MX', 'TXT', 'NS');--> statement-breakpoint +CREATE TYPE "public"."dns_resolver" AS ENUM('cloudflare', 'google');--> statement-breakpoint +CREATE TYPE "public"."provider_category" AS ENUM('hosting', 'email', 'dns', 'ca', 'registrar');--> statement-breakpoint +CREATE TABLE "certificates" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "domain_id" uuid NOT NULL, + "issuer" text NOT NULL, + "subject" text NOT NULL, + "alt_names" jsonb DEFAULT '[]'::jsonb NOT NULL, + "valid_from" timestamp with time zone NOT NULL, + "valid_to" timestamp with time zone NOT NULL, + "ca_provider_id" uuid, + "fetched_at" timestamp with time zone NOT NULL, + "expires_at" timestamp with time zone NOT NULL, + CONSTRAINT "ck_cert_valid_window" CHECK ("certificates"."valid_to" >= "certificates"."valid_from") +); +--> statement-breakpoint +CREATE TABLE "dns_records" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "domain_id" uuid NOT NULL, + "type" "dns_record_type" NOT NULL, + "name" text NOT NULL, + "value" text NOT NULL, + "ttl" integer, + "priority" integer, + "is_cloudflare" boolean, + "resolver" "dns_resolver" NOT NULL, + "fetched_at" timestamp with time zone NOT NULL, + "expires_at" timestamp with time zone NOT NULL, + CONSTRAINT "u_dns_record" UNIQUE("domain_id","type","name","value") +); +--> statement-breakpoint +CREATE TABLE "domains" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" text NOT NULL, + "tld" text NOT NULL, + "unicode_name" text NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "u_domains_name" UNIQUE("name") +); +--> statement-breakpoint +CREATE TABLE "hosting" ( + "domain_id" uuid PRIMARY KEY NOT NULL, + "hosting_provider_id" uuid, + "email_provider_id" uuid, + "dns_provider_id" uuid, + "geo_city" text, + "geo_region" text, + "geo_country" text, + "geo_country_code" text, + "geo_lat" double precision, + "geo_lon" double precision, + "fetched_at" timestamp with time zone NOT NULL, + "expires_at" timestamp with time zone NOT NULL +); +--> statement-breakpoint +CREATE TABLE "http_headers" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "domain_id" uuid NOT NULL, + "name" text NOT NULL, + "value" text NOT NULL, + "fetched_at" timestamp with time zone NOT NULL, + "expires_at" timestamp with time zone NOT NULL, + CONSTRAINT "u_http_header" UNIQUE("domain_id","name") +); +--> statement-breakpoint +CREATE TABLE "providers" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "category" "provider_category" NOT NULL, + "name" text NOT NULL, + "domain" text, + "slug" text NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "u_providers_category_slug" UNIQUE("category","slug") +); +--> statement-breakpoint +CREATE TABLE "registration_nameservers" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "domain_id" uuid NOT NULL, + "host" text NOT NULL, + "ipv4" jsonb DEFAULT '[]'::jsonb NOT NULL, + "ipv6" jsonb DEFAULT '[]'::jsonb NOT NULL, + CONSTRAINT "u_reg_ns" UNIQUE("domain_id","host") +); +--> statement-breakpoint +CREATE TABLE "registrations" ( + "domain_id" uuid PRIMARY KEY NOT NULL, + "is_registered" boolean NOT NULL, + "privacy_enabled" boolean, + "registry" text, + "creation_date" timestamp with time zone, + "updated_date" timestamp with time zone, + "expiration_date" timestamp with time zone, + "deletion_date" timestamp with time zone, + "transfer_lock" boolean, + "statuses" jsonb DEFAULT '[]'::jsonb NOT NULL, + "contacts" jsonb DEFAULT '{}'::jsonb NOT NULL, + "whois_server" text, + "rdap_servers" jsonb DEFAULT '[]'::jsonb NOT NULL, + "source" text NOT NULL, + "registrar_provider_id" uuid, + "reseller_provider_id" uuid, + "fetched_at" timestamp with time zone NOT NULL, + "expires_at" timestamp with time zone NOT NULL +); +--> statement-breakpoint +CREATE TABLE "seo" ( + "domain_id" uuid PRIMARY KEY NOT NULL, + "source_final_url" text, + "source_status" integer, + "meta_open_graph" jsonb DEFAULT '{}'::jsonb NOT NULL, + "meta_twitter" jsonb DEFAULT '{}'::jsonb NOT NULL, + "meta_general" jsonb DEFAULT '{}'::jsonb NOT NULL, + "preview_title" text, + "preview_description" text, + "preview_image_url" text, + "preview_image_uploaded_url" text, + "canonical_url" text, + "robots" jsonb DEFAULT '{}'::jsonb NOT NULL, + "robots_sitemaps" jsonb DEFAULT '[]'::jsonb NOT NULL, + "errors" jsonb DEFAULT '[]'::jsonb NOT NULL, + "fetched_at" timestamp with time zone NOT NULL, + "expires_at" timestamp with time zone NOT NULL +); +--> statement-breakpoint +ALTER TABLE "certificates" ADD CONSTRAINT "certificates_domain_id_domains_id_fk" FOREIGN KEY ("domain_id") REFERENCES "public"."domains"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "certificates" ADD CONSTRAINT "certificates_ca_provider_id_providers_id_fk" FOREIGN KEY ("ca_provider_id") REFERENCES "public"."providers"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "dns_records" ADD CONSTRAINT "dns_records_domain_id_domains_id_fk" FOREIGN KEY ("domain_id") REFERENCES "public"."domains"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "hosting" ADD CONSTRAINT "hosting_domain_id_domains_id_fk" FOREIGN KEY ("domain_id") REFERENCES "public"."domains"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "hosting" ADD CONSTRAINT "hosting_hosting_provider_id_providers_id_fk" FOREIGN KEY ("hosting_provider_id") REFERENCES "public"."providers"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "hosting" ADD CONSTRAINT "hosting_email_provider_id_providers_id_fk" FOREIGN KEY ("email_provider_id") REFERENCES "public"."providers"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "hosting" ADD CONSTRAINT "hosting_dns_provider_id_providers_id_fk" FOREIGN KEY ("dns_provider_id") REFERENCES "public"."providers"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "http_headers" ADD CONSTRAINT "http_headers_domain_id_domains_id_fk" FOREIGN KEY ("domain_id") REFERENCES "public"."domains"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "registration_nameservers" ADD CONSTRAINT "registration_nameservers_domain_id_domains_id_fk" FOREIGN KEY ("domain_id") REFERENCES "public"."domains"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "registrations" ADD CONSTRAINT "registrations_domain_id_domains_id_fk" FOREIGN KEY ("domain_id") REFERENCES "public"."domains"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "registrations" ADD CONSTRAINT "registrations_registrar_provider_id_providers_id_fk" FOREIGN KEY ("registrar_provider_id") REFERENCES "public"."providers"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "registrations" ADD CONSTRAINT "registrations_reseller_provider_id_providers_id_fk" FOREIGN KEY ("reseller_provider_id") REFERENCES "public"."providers"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "seo" ADD CONSTRAINT "seo_domain_id_domains_id_fk" FOREIGN KEY ("domain_id") REFERENCES "public"."domains"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "i_certs_domain" ON "certificates" USING btree ("domain_id");--> statement-breakpoint +CREATE INDEX "i_certs_valid_to" ON "certificates" USING btree ("valid_to");--> statement-breakpoint +CREATE INDEX "i_certs_expires" ON "certificates" USING btree ("expires_at");--> statement-breakpoint +CREATE INDEX "i_dns_domain_type" ON "dns_records" USING btree ("domain_id","type");--> statement-breakpoint +CREATE INDEX "i_dns_type_value" ON "dns_records" USING btree ("type","value");--> statement-breakpoint +CREATE INDEX "i_dns_expires" ON "dns_records" USING btree ("expires_at");--> statement-breakpoint +CREATE INDEX "i_domains_tld" ON "domains" USING btree ("tld");--> statement-breakpoint +CREATE INDEX "i_hosting_providers" ON "hosting" USING btree ("hosting_provider_id","email_provider_id","dns_provider_id");--> statement-breakpoint +CREATE INDEX "i_http_name" ON "http_headers" USING btree ("name");--> statement-breakpoint +CREATE INDEX "i_reg_ns_host" ON "registration_nameservers" USING btree ("host");--> statement-breakpoint +CREATE INDEX "i_reg_registrar" ON "registrations" USING btree ("registrar_provider_id");--> statement-breakpoint +CREATE INDEX "i_reg_expires" ON "registrations" USING btree ("expires_at");--> statement-breakpoint +CREATE INDEX "i_seo_src_final_url" ON "seo" USING btree ("source_final_url");--> statement-breakpoint +CREATE INDEX "i_seo_canonical" ON "seo" USING btree ("canonical_url"); \ No newline at end of file diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..b027cf2 --- /dev/null +++ b/drizzle/meta/0000_snapshot.json @@ -0,0 +1,1207 @@ +{ + "id": "bcc62004-d123-4026-8854-303595cfaad8", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.certificates": { + "name": "certificates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "domain_id": { + "name": "domain_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "alt_names": { + "name": "alt_names", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "valid_from": { + "name": "valid_from", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "valid_to": { + "name": "valid_to", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ca_provider_id": { + "name": "ca_provider_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "fetched_at": { + "name": "fetched_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "i_certs_domain": { + "name": "i_certs_domain", + "columns": [ + { + "expression": "domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "i_certs_valid_to": { + "name": "i_certs_valid_to", + "columns": [ + { + "expression": "valid_to", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "i_certs_expires": { + "name": "i_certs_expires", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "certificates_domain_id_domains_id_fk": { + "name": "certificates_domain_id_domains_id_fk", + "tableFrom": "certificates", + "tableTo": "domains", + "columnsFrom": [ + "domain_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "certificates_ca_provider_id_providers_id_fk": { + "name": "certificates_ca_provider_id_providers_id_fk", + "tableFrom": "certificates", + "tableTo": "providers", + "columnsFrom": [ + "ca_provider_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "ck_cert_valid_window": { + "name": "ck_cert_valid_window", + "value": "\"certificates\".\"valid_to\" >= \"certificates\".\"valid_from\"" + } + }, + "isRLSEnabled": false + }, + "public.dns_records": { + "name": "dns_records", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "domain_id": { + "name": "domain_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "dns_record_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ttl": { + "name": "ttl", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_cloudflare": { + "name": "is_cloudflare", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "resolver": { + "name": "resolver", + "type": "dns_resolver", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "fetched_at": { + "name": "fetched_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "i_dns_domain_type": { + "name": "i_dns_domain_type", + "columns": [ + { + "expression": "domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "i_dns_type_value": { + "name": "i_dns_type_value", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "value", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "i_dns_expires": { + "name": "i_dns_expires", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "dns_records_domain_id_domains_id_fk": { + "name": "dns_records_domain_id_domains_id_fk", + "tableFrom": "dns_records", + "tableTo": "domains", + "columnsFrom": [ + "domain_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "u_dns_record": { + "name": "u_dns_record", + "nullsNotDistinct": false, + "columns": [ + "domain_id", + "type", + "name", + "value" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.domains": { + "name": "domains", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tld": { + "name": "tld", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "unicode_name": { + "name": "unicode_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "i_domains_tld": { + "name": "i_domains_tld", + "columns": [ + { + "expression": "tld", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "u_domains_name": { + "name": "u_domains_name", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hosting": { + "name": "hosting", + "schema": "", + "columns": { + "domain_id": { + "name": "domain_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "hosting_provider_id": { + "name": "hosting_provider_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "email_provider_id": { + "name": "email_provider_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "dns_provider_id": { + "name": "dns_provider_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "geo_city": { + "name": "geo_city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "geo_region": { + "name": "geo_region", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "geo_country": { + "name": "geo_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "geo_country_code": { + "name": "geo_country_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "geo_lat": { + "name": "geo_lat", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "geo_lon": { + "name": "geo_lon", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "fetched_at": { + "name": "fetched_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "i_hosting_providers": { + "name": "i_hosting_providers", + "columns": [ + { + "expression": "hosting_provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email_provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dns_provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "hosting_domain_id_domains_id_fk": { + "name": "hosting_domain_id_domains_id_fk", + "tableFrom": "hosting", + "tableTo": "domains", + "columnsFrom": [ + "domain_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "hosting_hosting_provider_id_providers_id_fk": { + "name": "hosting_hosting_provider_id_providers_id_fk", + "tableFrom": "hosting", + "tableTo": "providers", + "columnsFrom": [ + "hosting_provider_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hosting_email_provider_id_providers_id_fk": { + "name": "hosting_email_provider_id_providers_id_fk", + "tableFrom": "hosting", + "tableTo": "providers", + "columnsFrom": [ + "email_provider_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hosting_dns_provider_id_providers_id_fk": { + "name": "hosting_dns_provider_id_providers_id_fk", + "tableFrom": "hosting", + "tableTo": "providers", + "columnsFrom": [ + "dns_provider_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.http_headers": { + "name": "http_headers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "domain_id": { + "name": "domain_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fetched_at": { + "name": "fetched_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "i_http_name": { + "name": "i_http_name", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "http_headers_domain_id_domains_id_fk": { + "name": "http_headers_domain_id_domains_id_fk", + "tableFrom": "http_headers", + "tableTo": "domains", + "columnsFrom": [ + "domain_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "u_http_header": { + "name": "u_http_header", + "nullsNotDistinct": false, + "columns": [ + "domain_id", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.providers": { + "name": "providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "category": { + "name": "category", + "type": "provider_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "u_providers_category_slug": { + "name": "u_providers_category_slug", + "nullsNotDistinct": false, + "columns": [ + "category", + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.registration_nameservers": { + "name": "registration_nameservers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "domain_id": { + "name": "domain_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ipv4": { + "name": "ipv4", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "ipv6": { + "name": "ipv6", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "i_reg_ns_host": { + "name": "i_reg_ns_host", + "columns": [ + { + "expression": "host", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "registration_nameservers_domain_id_domains_id_fk": { + "name": "registration_nameservers_domain_id_domains_id_fk", + "tableFrom": "registration_nameservers", + "tableTo": "domains", + "columnsFrom": [ + "domain_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "u_reg_ns": { + "name": "u_reg_ns", + "nullsNotDistinct": false, + "columns": [ + "domain_id", + "host" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.registrations": { + "name": "registrations", + "schema": "", + "columns": { + "domain_id": { + "name": "domain_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "is_registered": { + "name": "is_registered", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "privacy_enabled": { + "name": "privacy_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "registry": { + "name": "registry", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "creation_date": { + "name": "creation_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_date": { + "name": "updated_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expiration_date": { + "name": "expiration_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deletion_date": { + "name": "deletion_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "transfer_lock": { + "name": "transfer_lock", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "statuses": { + "name": "statuses", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "contacts": { + "name": "contacts", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "whois_server": { + "name": "whois_server", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdap_servers": { + "name": "rdap_servers", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "registrar_provider_id": { + "name": "registrar_provider_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reseller_provider_id": { + "name": "reseller_provider_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "fetched_at": { + "name": "fetched_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "i_reg_registrar": { + "name": "i_reg_registrar", + "columns": [ + { + "expression": "registrar_provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "i_reg_expires": { + "name": "i_reg_expires", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "registrations_domain_id_domains_id_fk": { + "name": "registrations_domain_id_domains_id_fk", + "tableFrom": "registrations", + "tableTo": "domains", + "columnsFrom": [ + "domain_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "registrations_registrar_provider_id_providers_id_fk": { + "name": "registrations_registrar_provider_id_providers_id_fk", + "tableFrom": "registrations", + "tableTo": "providers", + "columnsFrom": [ + "registrar_provider_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "registrations_reseller_provider_id_providers_id_fk": { + "name": "registrations_reseller_provider_id_providers_id_fk", + "tableFrom": "registrations", + "tableTo": "providers", + "columnsFrom": [ + "reseller_provider_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.seo": { + "name": "seo", + "schema": "", + "columns": { + "domain_id": { + "name": "domain_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "source_final_url": { + "name": "source_final_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_status": { + "name": "source_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "meta_open_graph": { + "name": "meta_open_graph", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "meta_twitter": { + "name": "meta_twitter", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "meta_general": { + "name": "meta_general", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "preview_title": { + "name": "preview_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_description": { + "name": "preview_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_image_url": { + "name": "preview_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_image_uploaded_url": { + "name": "preview_image_uploaded_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "canonical_url": { + "name": "canonical_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "robots": { + "name": "robots", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "robots_sitemaps": { + "name": "robots_sitemaps", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "errors": { + "name": "errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "fetched_at": { + "name": "fetched_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "i_seo_src_final_url": { + "name": "i_seo_src_final_url", + "columns": [ + { + "expression": "source_final_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "i_seo_canonical": { + "name": "i_seo_canonical", + "columns": [ + { + "expression": "canonical_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "seo_domain_id_domains_id_fk": { + "name": "seo_domain_id_domains_id_fk", + "tableFrom": "seo", + "tableTo": "domains", + "columnsFrom": [ + "domain_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.dns_record_type": { + "name": "dns_record_type", + "schema": "public", + "values": [ + "A", + "AAAA", + "MX", + "TXT", + "NS" + ] + }, + "public.dns_resolver": { + "name": "dns_resolver", + "schema": "public", + "values": [ + "cloudflare", + "google" + ] + }, + "public.provider_category": { + "name": "provider_category", + "schema": "public", + "values": [ + "hosting", + "email", + "dns", + "ca", + "registrar" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json new file mode 100644 index 0000000..a2bdd07 --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1760758697218, + "tag": "0000_nosy_wendell_rand", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/lib/schemas/domain/registration.ts b/lib/schemas/domain/registration.ts index 4d3c455..4706c73 100644 --- a/lib/schemas/domain/registration.ts +++ b/lib/schemas/domain/registration.ts @@ -6,7 +6,6 @@ export const RegistrationSchema = z.object({ domain: z.string(), tld: z.string(), isRegistered: z.boolean(), - isIDN: z.boolean().optional(), unicodeName: z.string().optional(), punycodeName: z.string().optional(), registry: z.string().optional(), diff --git a/lib/schemas/index.ts b/lib/schemas/index.ts index 7d700b5..d6c6321 100644 --- a/lib/schemas/index.ts +++ b/lib/schemas/index.ts @@ -7,4 +7,5 @@ export * from "./domain/registration"; export * from "./domain/seo"; export * from "./internal/export"; export * from "./internal/provider"; +export * from "./internal/sections"; export * from "./internal/storage"; diff --git a/lib/schemas/internal/sections.ts b/lib/schemas/internal/sections.ts new file mode 100644 index 0000000..a1d2937 --- /dev/null +++ b/lib/schemas/internal/sections.ts @@ -0,0 +1,12 @@ +import { z } from "zod"; + +export const SectionEnum = z.enum([ + "dns", + "headers", + "hosting", + "certificates", + "seo", + "registration", +]); + +export type Section = z.infer; diff --git a/package.json b/package.json index f852749..6a24a3d 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,11 @@ "lint": "biome check", "format": "biome format --write", "typecheck": "tsc --noEmit", + "db:generate": "drizzle-kit generate", + "db:push": "drizzle-kit push", + "db:migrate": "drizzle-kit migrate", + "db:studio": "drizzle-kit studio", + "db:seed:providers": "tsx server/db/seed/providers.ts", "test": "vitest", "test:run": "vitest run", "test:ui": "vitest --ui", @@ -25,35 +30,38 @@ }, "dependencies": { "@date-fns/utc": "^2.1.1", - "@posthog/nextjs-config": "^1.3.3", + "@neondatabase/serverless": "^1.0.2", + "@posthog/nextjs-config": "^1.3.4", "@sparticuz/chromium": "140.0.0", - "@tanstack/react-query": "^5.90.3", + "@tanstack/react-query": "^5.90.5", "@tanstack/react-query-devtools": "^5.90.2", "@trpc/client": "^11.6.0", "@trpc/server": "^11.6.0", "@trpc/tanstack-react-query": "^11.6.0", "@upstash/redis": "^1.35.6", "@vercel/analytics": "^1.5.0", - "@vercel/functions": "^3.1.3", + "@vercel/functions": "^3.1.4", "cheerio": "^1.1.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "country-flag-icons": "^1.5.21", "date-fns": "^4.1.0", + "drizzle-orm": "^0.44.6", "geist": "^1.5.1", - "icojs": "^0.19.5", + "icojs": "^0.20.0", + "inngest": "^3.44.3", "ipaddr.js": "^2.2.0", - "lucide-react": "^0.545.0", + "lucide-react": "^0.546.0", "mapbox-gl": "^3.15.0", "motion": "^12.23.24", "next": "15.6.0-canary.39", "next-themes": "^0.4.6", - "posthog-js": "^1.275.2", + "posthog-js": "^1.276.0", "posthog-node": "^5.10.0", "puppeteer-core": "24.22.3", "radix-ui": "^1.4.3", - "rdapper": "^0.7.0", + "rdapper": "^0.8.0", "react": "19.1.1", "react-dom": "19.1.1", "react-map-gl": "^8.1.0", @@ -69,21 +77,24 @@ }, "devDependencies": { "@biomejs/biome": "2.2.6", + "@electric-sql/pglite": "^0.3.11", "@tailwindcss/postcss": "^4.1.14", "@testing-library/dom": "10.4.1", "@testing-library/jest-dom": "6.9.1", "@testing-library/react": "16.3.0", "@testing-library/user-event": "14.6.1", - "@types/node": "24.7.2", + "@types/node": "24.8.1", "@types/react": "19.1.16", "@types/react-dom": "19.1.9", "@vitejs/plugin-react": "^5.0.4", "@vitest/coverage-v8": "^3.2.4", "@vitest/ui": "^3.2.4", "babel-plugin-react-compiler": "19.1.0-rc.3", + "drizzle-kit": "^0.31.5", "jsdom": "^27.0.0", "puppeteer": "24.22.3", "tailwindcss": "^4.1.14", + "tsx": "^4.20.6", "tw-animate-css": "^1.4.0", "typescript": "5.9.3", "vite-tsconfig-paths": "^5.1.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f8aa673..464748f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,18 +11,21 @@ importers: '@date-fns/utc': specifier: ^2.1.1 version: 2.1.1 + '@neondatabase/serverless': + specifier: ^1.0.2 + version: 1.0.2 '@posthog/nextjs-config': - specifier: ^1.3.3 - version: 1.3.3(next@15.6.0-canary.39(@babel/core@7.28.4)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)) + specifier: ^1.3.4 + version: 1.3.4(next@15.6.0-canary.39(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)) '@sparticuz/chromium': specifier: 140.0.0 version: 140.0.0 '@tanstack/react-query': - specifier: ^5.90.3 - version: 5.90.3(react@19.1.1) + specifier: ^5.90.5 + version: 5.90.5(react@19.1.1) '@tanstack/react-query-devtools': specifier: ^5.90.2 - version: 5.90.2(@tanstack/react-query@5.90.3(react@19.1.1))(react@19.1.1) + version: 5.90.2(@tanstack/react-query@5.90.5(react@19.1.1))(react@19.1.1) '@trpc/client': specifier: ^11.6.0 version: 11.6.0(@trpc/server@11.6.0(typescript@5.9.3))(typescript@5.9.3) @@ -31,16 +34,16 @@ importers: version: 11.6.0(typescript@5.9.3) '@trpc/tanstack-react-query': specifier: ^11.6.0 - version: 11.6.0(@tanstack/react-query@5.90.3(react@19.1.1))(@trpc/client@11.6.0(@trpc/server@11.6.0(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.6.0(typescript@5.9.3))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.9.3) + version: 11.6.0(@tanstack/react-query@5.90.5(react@19.1.1))(@trpc/client@11.6.0(@trpc/server@11.6.0(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.6.0(typescript@5.9.3))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.9.3) '@upstash/redis': specifier: ^1.35.6 version: 1.35.6 '@vercel/analytics': specifier: ^1.5.0 - version: 1.5.0(next@15.6.0-canary.39(@babel/core@7.28.4)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1) + version: 1.5.0(next@15.6.0-canary.39(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1) '@vercel/functions': - specifier: ^3.1.3 - version: 3.1.3 + specifier: ^3.1.4 + version: 3.1.4 cheerio: specifier: ^1.1.2 version: 1.1.2 @@ -59,18 +62,24 @@ importers: date-fns: specifier: ^4.1.0 version: 4.1.0 + drizzle-orm: + specifier: ^0.44.6 + version: 0.44.6(@electric-sql/pglite@0.3.11)(@neondatabase/serverless@1.0.2)(@opentelemetry/api@1.9.0)(@types/pg@8.15.5)(@upstash/redis@1.35.6) geist: specifier: ^1.5.1 - version: 1.5.1(next@15.6.0-canary.39(@babel/core@7.28.4)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)) + version: 1.5.1(next@15.6.0-canary.39(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)) icojs: - specifier: ^0.19.5 - version: 0.19.5(@jimp/custom@0.22.12) + specifier: ^0.20.0 + version: 0.20.0(@jimp/custom@0.22.12) + inngest: + specifier: ^3.44.3 + version: 3.44.3(next@15.6.0-canary.39(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(typescript@5.9.3)(zod@4.1.12) ipaddr.js: specifier: ^2.2.0 version: 2.2.0 lucide-react: - specifier: ^0.545.0 - version: 0.545.0(react@19.1.1) + specifier: ^0.546.0 + version: 0.546.0(react@19.1.1) mapbox-gl: specifier: ^3.15.0 version: 3.15.0 @@ -79,13 +88,13 @@ importers: version: 12.23.24(react-dom@19.1.1(react@19.1.1))(react@19.1.1) next: specifier: 15.6.0-canary.39 - version: 15.6.0-canary.39(@babel/core@7.28.4)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 15.6.0-canary.39(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1) posthog-js: - specifier: ^1.275.2 - version: 1.275.2 + specifier: ^1.276.0 + version: 1.276.0 posthog-node: specifier: ^5.10.0 version: 5.10.0 @@ -96,8 +105,8 @@ importers: specifier: ^1.4.3 version: 1.4.3(@types/react-dom@19.1.9(@types/react@19.1.16))(@types/react@19.1.16)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) rdapper: - specifier: ^0.7.0 - version: 0.7.0 + specifier: ^0.8.0 + version: 0.8.0 react: specifier: 19.1.1 version: 19.1.1 @@ -124,7 +133,7 @@ importers: version: 3.3.1 uploadthing: specifier: ^7.7.4 - version: 7.7.4(next@15.6.0-canary.39(@babel/core@7.28.4)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(tailwindcss@4.1.14) + version: 7.7.4(next@15.6.0-canary.39(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(tailwindcss@4.1.14) uuid: specifier: ^13.0.0 version: 13.0.0 @@ -138,6 +147,9 @@ importers: '@biomejs/biome': specifier: 2.2.6 version: 2.2.6 + '@electric-sql/pglite': + specifier: ^0.3.11 + version: 0.3.11 '@tailwindcss/postcss': specifier: ^4.1.14 version: 4.1.14 @@ -154,8 +166,8 @@ importers: specifier: 14.6.1 version: 14.6.1(@testing-library/dom@10.4.1) '@types/node': - specifier: 24.7.2 - version: 24.7.2 + specifier: 24.8.1 + version: 24.8.1 '@types/react': specifier: 19.1.16 version: 19.1.16 @@ -164,7 +176,7 @@ importers: version: 19.1.9(@types/react@19.1.16) '@vitejs/plugin-react': specifier: ^5.0.4 - version: 5.0.4(vite@7.1.10(@types/node@24.7.2)(jiti@2.6.1)(lightningcss@1.30.1)) + version: 5.0.4(vite@7.1.10(@types/node@24.8.1)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)) '@vitest/coverage-v8': specifier: ^3.2.4 version: 3.2.4(vitest@3.2.4) @@ -174,6 +186,9 @@ importers: babel-plugin-react-compiler: specifier: 19.1.0-rc.3 version: 19.1.0-rc.3 + drizzle-kit: + specifier: ^0.31.5 + version: 0.31.5 jsdom: specifier: ^27.0.0 version: 27.0.0(postcss@8.5.6) @@ -183,6 +198,9 @@ importers: tailwindcss: specifier: ^4.1.14 version: 4.1.14 + tsx: + specifier: ^4.20.6 + version: 4.20.6 tw-animate-css: specifier: ^1.4.0 version: 1.4.0 @@ -191,10 +209,10 @@ importers: version: 5.9.3 vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.9.3)(vite@7.1.10(@types/node@24.7.2)(jiti@2.6.1)(lightningcss@1.30.1)) + version: 5.1.4(typescript@5.9.3)(vite@7.1.10(@types/node@24.8.1)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)) vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@24.7.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.8.1)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.1)(tsx@4.20.6) packages: @@ -212,8 +230,8 @@ packages: '@asamuzakjp/css-color@4.0.5': resolution: {integrity: sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==} - '@asamuzakjp/dom-selector@6.6.2': - resolution: {integrity: sha512-+AG0jN9HTwfDLBhjhX1FKi6zlIAc/YGgEHlN/OMaHD1pOPFsC5CpYQpLkPX0aFjyaVmoq9330cQDCU4qnSL1qA==} + '@asamuzakjp/dom-selector@6.7.2': + resolution: {integrity: sha512-ccKogJI+0aiDhOahdjANIc9SDixSud1gbwdVrhn7kMopAtLXqsz9MKmQQtIl6Y5aC2IYq+j4dz/oedL2AVMmVQ==} '@asamuzakjp/nwsapi@2.3.9': resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} @@ -365,6 +383,9 @@ packages: '@borewit/text-codec@0.1.1': resolution: {integrity: sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==} + '@bufbuild/protobuf@2.9.0': + resolution: {integrity: sha512-rnJenoStJ8nvmt9Gzye8nkYd6V22xUAnu4086ER7h1zJ508vStko4pMvDeQ446ilDTFpV5wnoc5YS7XvMwwMqA==} + '@canvas/image-data@1.1.0': resolution: {integrity: sha512-QdObRRjRbcXGmM1tmJ+MrHcaz1MftF2+W7YI+MsphnsCrmtyfS0d5qJbk0MeSbUeyM/jCb0hmnkXPsy026L7dA==} @@ -405,166 +426,312 @@ packages: '@date-fns/utc@2.1.1': resolution: {integrity: sha512-SlJDfG6RPeEX8wEVv6ZB3kak4MmbtyiI2qX/5zuKdordbrhB/iaJ58GVMZgJ6P1sJaM1gMgENFYYeg1JWrCFrA==} + '@drizzle-team/brocli@0.10.2': + resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + '@effect/platform@0.90.3': resolution: {integrity: sha512-XvQ37yzWQKih4Du2CYladd1i/MzqtgkTPNCaN6Ku6No4CK83hDtXIV/rP03nEoBg2R3Pqgz6gGWmE2id2G81HA==} peerDependencies: effect: ^3.17.7 + '@electric-sql/pglite@0.3.11': + resolution: {integrity: sha512-FJtjnEyez8XgmgyE5Ewmx89TGVN+75ZjykFoExApRIbJBMT4dsbsuZkF/YWLuymGDfGFHDACjvENPMEqg4FoWg==} + '@emnapi/runtime@1.5.0': resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==} - '@esbuild/aix-ppc64@0.25.10': - resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==} + '@esbuild-kit/core-utils@3.3.2': + resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild-kit/esm-loader@2.6.5': + resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild/aix-ppc64@0.25.11': + resolution: {integrity: sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.25.10': - resolution: {integrity: sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==} + '@esbuild/android-arm64@0.18.20': + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.25.11': + resolution: {integrity: sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.25.10': - resolution: {integrity: sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==} + '@esbuild/android-arm@0.18.20': + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.25.11': + resolution: {integrity: sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.25.10': - resolution: {integrity: sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==} + '@esbuild/android-x64@0.18.20': + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.25.11': + resolution: {integrity: sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.25.10': - resolution: {integrity: sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==} + '@esbuild/darwin-arm64@0.18.20': + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.25.11': + resolution: {integrity: sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.25.10': - resolution: {integrity: sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==} + '@esbuild/darwin-x64@0.18.20': + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.11': + resolution: {integrity: sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.25.10': - resolution: {integrity: sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==} + '@esbuild/freebsd-arm64@0.18.20': + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.25.11': + resolution: {integrity: sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.10': - resolution: {integrity: sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==} + '@esbuild/freebsd-x64@0.18.20': + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.11': + resolution: {integrity: sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.25.10': - resolution: {integrity: sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==} + '@esbuild/linux-arm64@0.18.20': + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.25.11': + resolution: {integrity: sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.25.10': - resolution: {integrity: sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==} + '@esbuild/linux-arm@0.18.20': + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.25.11': + resolution: {integrity: sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.25.10': - resolution: {integrity: sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==} + '@esbuild/linux-ia32@0.18.20': + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.25.11': + resolution: {integrity: sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.25.10': - resolution: {integrity: sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==} + '@esbuild/linux-loong64@0.18.20': + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.25.11': + resolution: {integrity: sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.25.10': - resolution: {integrity: sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==} + '@esbuild/linux-mips64el@0.18.20': + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.25.11': + resolution: {integrity: sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.25.10': - resolution: {integrity: sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==} + '@esbuild/linux-ppc64@0.18.20': + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.25.11': + resolution: {integrity: sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.25.10': - resolution: {integrity: sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==} + '@esbuild/linux-riscv64@0.18.20': + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.11': + resolution: {integrity: sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.25.10': - resolution: {integrity: sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==} + '@esbuild/linux-s390x@0.18.20': + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.25.11': + resolution: {integrity: sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.25.10': - resolution: {integrity: sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==} + '@esbuild/linux-x64@0.18.20': + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.25.11': + resolution: {integrity: sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.10': - resolution: {integrity: sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==} + '@esbuild/netbsd-arm64@0.25.11': + resolution: {integrity: sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.10': - resolution: {integrity: sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==} + '@esbuild/netbsd-x64@0.18.20': + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.11': + resolution: {integrity: sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.10': - resolution: {integrity: sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==} + '@esbuild/openbsd-arm64@0.25.11': + resolution: {integrity: sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.10': - resolution: {integrity: sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==} + '@esbuild/openbsd-x64@0.18.20': + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.11': + resolution: {integrity: sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.25.10': - resolution: {integrity: sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==} + '@esbuild/openharmony-arm64@0.25.11': + resolution: {integrity: sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.25.10': - resolution: {integrity: sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==} + '@esbuild/sunos-x64@0.18.20': + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.25.11': + resolution: {integrity: sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.25.10': - resolution: {integrity: sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==} + '@esbuild/win32-arm64@0.18.20': + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.25.11': + resolution: {integrity: sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.25.10': - resolution: {integrity: sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==} + '@esbuild/win32-ia32@0.18.20': + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.25.11': + resolution: {integrity: sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.25.10': - resolution: {integrity: sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==} + '@esbuild/win32-x64@0.18.20': + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.25.11': + resolution: {integrity: sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -584,6 +751,15 @@ packages: '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + '@grpc/grpc-js@1.14.0': + resolution: {integrity: sha512-N8Jx6PaYzcTRNzirReJCtADVoq4z7+1KQ4E70jTg/koQiMoUSN1kbNjPOqpPbhMFhfU1/l7ixspPl8dNY+FoUg==} + engines: {node: '>=12.10.0'} + + '@grpc/proto-loader@0.8.0': + resolution: {integrity: sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==} + engines: {node: '>=6'} + hasBin: true + '@img/colour@1.0.0': resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} engines: {node: '>=18'} @@ -710,6 +886,9 @@ packages: cpu: [x64] os: [win32] + '@inngest/ai@0.1.7': + resolution: {integrity: sha512-5xWatW441jacGf9czKEZdgAmkvoy7GS2tp7X8GSbdGeRXzjisHR6vM+q8DQbv6rqRsmQoCQ5iShh34MguELvUQ==} + '@isaacs/balanced-match@4.0.1': resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} engines: {node: 20 || >=22} @@ -744,6 +923,9 @@ packages: '@jimp/utils@0.22.12': resolution: {integrity: sha512-yJ5cWUknGnilBq97ZXOyOS0HhsHOyAyjHwYfHxGbSyMTohgQI6sVyE8KPgDwH8HHW/nMKXk8TrSwAE71zt716Q==} + '@jpwilliams/waitgroup@2.1.1': + resolution: {integrity: sha512-0CxRhNfkvFCTLZBKGvKxY2FYtYW1yWhO2McLqBL0X5UWvYjIf9suH8anKW/DNutl369A75Ewyoh2iJMwBZ2tRg==} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -760,6 +942,9 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@js-sdsl/ordered-map@4.4.2': + resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + '@mapbox/jsonlint-lines-primitives@2.0.2': resolution: {integrity: sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==} engines: {node: '>= 0.6'} @@ -817,6 +1002,10 @@ packages: cpu: [x64] os: [win32] + '@neondatabase/serverless@1.0.2': + resolution: {integrity: sha512-I5sbpSIAHiB+b6UttofhrN/UJXII+4tZPAq1qugzwCwLIL8EZLV7F/JyHUrEIiGgQpEXzpnjlJ+zwcEhheGvCw==} + engines: {node: '>=19.0.0'} + '@next/env@15.6.0-canary.39': resolution: {integrity: sha512-WvJxtTel5Yt+z1QmCfgdFTOwk3edEzvhh6G1AuV45g2JJfx/8PljYEGVApJlUe2pjfKpW3K3K56qmeaaa+h7pw==} @@ -868,10 +1057,458 @@ packages: cpu: [x64] os: [win32] + '@opentelemetry/api-logs@0.57.2': + resolution: {integrity: sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A==} + engines: {node: '>=14'} + + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/auto-instrumentations-node@0.56.1': + resolution: {integrity: sha512-4cK0+unfkXRRbQQg2r9K3ki8JlE0j9Iw8+4DZEkChShAnmviiE+/JMgHGvK+VVcLrSlgV6BBHv4+ZTLukQwhkA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.4.1 + + '@opentelemetry/context-async-hooks@1.30.1': + resolution: {integrity: sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@1.30.1': + resolution: {integrity: sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/exporter-logs-otlp-grpc@0.57.2': + resolution: {integrity: sha512-eovEy10n3umjKJl2Ey6TLzikPE+W4cUQ4gCwgGP1RqzTGtgDra0WjIqdy29ohiUKfvmbiL3MndZww58xfIvyFw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-logs-otlp-http@0.57.2': + resolution: {integrity: sha512-0rygmvLcehBRp56NQVLSleJ5ITTduq/QfU7obOkyWgPpFHulwpw2LYTqNIz5TczKZuy5YY+5D3SDnXZL1tXImg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-logs-otlp-proto@0.57.2': + resolution: {integrity: sha512-ta0ithCin0F8lu9eOf4lEz9YAScecezCHkMMyDkvd9S7AnZNX5ikUmC5EQOQADU+oCcgo/qkQIaKcZvQ0TYKDw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-grpc@0.57.2': + resolution: {integrity: sha512-r70B8yKR41F0EC443b5CGB4rUaOMm99I5N75QQt6sHKxYDzSEc6gm48Diz1CI1biwa5tDPznpylTrywO/pT7qw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-http@0.57.2': + resolution: {integrity: sha512-ttb9+4iKw04IMubjm3t0EZsYRNWr3kg44uUuzfo9CaccYlOh8cDooe4QObDUkvx9d5qQUrbEckhrWKfJnKhemA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-proto@0.57.2': + resolution: {integrity: sha512-HX068Q2eNs38uf7RIkNN9Hl4Ynl+3lP0++KELkXMCpsCbFO03+0XNNZ1SkwxPlP9jrhQahsMPMkzNXpq3fKsnw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-prometheus@0.57.2': + resolution: {integrity: sha512-VqIqXnuxWMWE/1NatAGtB1PvsQipwxDcdG4RwA/umdBcW3/iOHp0uejvFHTRN2O78ZPged87ErJajyUBPUhlDQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-grpc@0.57.2': + resolution: {integrity: sha512-gHU1vA3JnHbNxEXg5iysqCWxN9j83d7/epTYBZflqQnTyCC4N7yZXn/dMM+bEmyhQPGjhCkNZLx4vZuChH1PYw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-http@0.57.2': + resolution: {integrity: sha512-sB/gkSYFu+0w2dVQ0PWY9fAMl172PKMZ/JrHkkW8dmjCL0CYkmXeE+ssqIL/yBUTPOvpLIpenX5T9RwXRBW/3g==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-proto@0.57.2': + resolution: {integrity: sha512-awDdNRMIwDvUtoRYxRhja5QYH6+McBLtoz1q9BeEsskhZcrGmH/V1fWpGx8n+Rc+542e8pJA6y+aullbIzQmlw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-zipkin@1.30.1': + resolution: {integrity: sha512-6S2QIMJahIquvFaaxmcwpvQQRD/YFaMTNoIxrfPIPOeITN+a8lfEcPDxNxn8JDAaxkg+4EnXhz8upVDYenoQjA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/instrumentation-amqplib@0.46.1': + resolution: {integrity: sha512-AyXVnlCf/xV3K/rNumzKxZqsULyITJH6OVLiW6730JPRqWA7Zc9bvYoVNpN6iOpTU8CasH34SU/ksVJmObFibQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-aws-lambda@0.50.3': + resolution: {integrity: sha512-kotm/mRvSWUauudxcylc5YCDei+G/r+jnOH6q5S99aPLQ/Ms8D2yonMIxEJUILIPlthEmwLYxkw3ualWzMjm/A==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-aws-sdk@0.49.1': + resolution: {integrity: sha512-Vbj4BYeV/1K4Pbbfk+gQ8gwYL0w+tBeUwG88cOxnF7CLPO1XnskGV8Q3Gzut2Ah/6Dg17dBtlzEqL3UiFP2Z6A==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-bunyan@0.45.1': + resolution: {integrity: sha512-T9POV9ccS41UjpsjLrJ4i0m8LfplBiN3dMeH9XZ2btiDrjoaWtDrst6tNb1avetBjkeshOuBp1EWKP22EVSr0g==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-cassandra-driver@0.45.1': + resolution: {integrity: sha512-RqnP0rK2hcKK1AKcmYvedLiL6G5TvFGiSUt2vI9wN0cCBdTt9Y9+wxxY19KoGxq7e9T/aHow6P5SUhCVI1sHvQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-connect@0.43.1': + resolution: {integrity: sha512-ht7YGWQuV5BopMcw5Q2hXn3I8eG8TH0J/kc/GMcW4CuNTgiP6wCu44BOnucJWL3CmFWaRHI//vWyAhaC8BwePw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-cucumber@0.14.1': + resolution: {integrity: sha512-ybO+tmH85pDO0ywTskmrMtZcccKyQr7Eb7wHy1keR2HFfx46SzZbjHo1AuGAX//Hook3gjM7+w211gJ2bwKe1Q==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/instrumentation-dataloader@0.16.1': + resolution: {integrity: sha512-K/qU4CjnzOpNkkKO4DfCLSQshejRNAJtd4esgigo/50nxCB6XCyi1dhAblUHM9jG5dRm8eu0FB+t87nIo99LYQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-dns@0.43.1': + resolution: {integrity: sha512-e/tMZYU1nc+k+J3259CQtqVZIPsPRSLNoAQbGEmSKrjLEY/KJSbpBZ17lu4dFVBzqoF1cZYIZxn9WPQxy4V9ng==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-express@0.47.1': + resolution: {integrity: sha512-QNXPTWteDclR2B4pDFpz0TNghgB33UMjUt14B+BZPmtH1MwUFAfLHBaP5If0Z5NZC+jaH8oF2glgYjrmhZWmSw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-fastify@0.44.2': + resolution: {integrity: sha512-arSp97Y4D2NWogoXRb8CzFK3W2ooVdvqRRtQDljFt9uC3zI6OuShgey6CVFC0JxT1iGjkAr1r4PDz23mWrFULQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-fs@0.19.1': + resolution: {integrity: sha512-6g0FhB3B9UobAR60BGTcXg4IHZ6aaYJzp0Ki5FhnxyAPt8Ns+9SSvgcrnsN2eGmk3RWG5vYycUGOEApycQL24A==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-generic-pool@0.43.1': + resolution: {integrity: sha512-M6qGYsp1cURtvVLGDrPPZemMFEbuMmCXgQYTReC/IbimV5sGrLBjB+/hANUpRZjX67nGLdKSVLZuQQAiNz+sww==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-graphql@0.47.1': + resolution: {integrity: sha512-EGQRWMGqwiuVma8ZLAZnExQ7sBvbOx0N/AE/nlafISPs8S+QtXX+Viy6dcQwVWwYHQPAcuY3bFt3xgoAwb4ZNQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-grpc@0.57.2': + resolution: {integrity: sha512-TR6YQA67cLSZzdxbf2SrbADJy2Y8eBW1+9mF15P0VK2MYcpdoUSmQTF1oMkBwa3B9NwqDFA2fq7wYTTutFQqaQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-hapi@0.45.2': + resolution: {integrity: sha512-7Ehow/7Wp3aoyCrZwQpU7a2CnoMq0XhIcioFuKjBb0PLYfBfmTsFTUyatlHu0fRxhwcRsSQRTvEhmZu8CppBpQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-http@0.57.2': + resolution: {integrity: sha512-1Uz5iJ9ZAlFOiPuwYg29Bf7bJJc/GeoeJIFKJYQf67nTVKFe8RHbEtxgkOmK4UGZNHKXcpW4P8cWBYzBn1USpg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-ioredis@0.47.1': + resolution: {integrity: sha512-OtFGSN+kgk/aoKgdkKQnBsQFDiG8WdCxu+UrHr0bXScdAmtSzLSraLo7wFIb25RVHfRWvzI5kZomqJYEg/l1iA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-kafkajs@0.7.1': + resolution: {integrity: sha512-OtjaKs8H7oysfErajdYr1yuWSjMAectT7Dwr+axIoZqT9lmEOkD/H/3rgAs8h/NIuEi2imSXD+vL4MZtOuJfqQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-knex@0.44.1': + resolution: {integrity: sha512-U4dQxkNhvPexffjEmGwCq68FuftFK15JgUF05y/HlK3M6W/G2iEaACIfXdSnwVNe9Qh0sPfw8LbOPxrWzGWGMQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-koa@0.47.1': + resolution: {integrity: sha512-l/c+Z9F86cOiPJUllUCt09v+kICKvT+Vg1vOAJHtHPsJIzurGayucfCMq2acd/A/yxeNWunl9d9eqZ0G+XiI6A==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-lru-memoizer@0.44.1': + resolution: {integrity: sha512-5MPkYCvG2yw7WONEjYj5lr5JFehTobW7wX+ZUFy81oF2lr9IPfZk9qO+FTaM0bGEiymwfLwKe6jE15nHn1nmHg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-memcached@0.43.1': + resolution: {integrity: sha512-rK5YWC22gmsLp2aEbaPk5F+9r6BFFZuc9GTnW/ErrWpz2XNHUgeFInoPDg4t+Trs8OttIfn8XwkfFkSKqhxanw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mongodb@0.52.0': + resolution: {integrity: sha512-1xmAqOtRUQGR7QfJFfGV/M2kC7wmI2WgZdpru8hJl3S0r4hW0n3OQpEHlSGXJAaNFyvT+ilnwkT+g5L4ljHR6g==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mongoose@0.46.1': + resolution: {integrity: sha512-3kINtW1LUTPkiXFRSSBmva1SXzS/72we/jL22N+BnF3DFcoewkdkHPYOIdAAk9gSicJ4d5Ojtt1/HeibEc5OQg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mysql2@0.45.2': + resolution: {integrity: sha512-h6Ad60FjCYdJZ5DTz1Lk2VmQsShiViKe0G7sYikb0GHI0NVvApp2XQNRHNjEMz87roFttGPLHOYVPlfy+yVIhQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mysql@0.45.1': + resolution: {integrity: sha512-TKp4hQ8iKQsY7vnp/j0yJJ4ZsP109Ht6l4RHTj0lNEG1TfgTrIH5vJMbgmoYXWzNHAqBH2e7fncN12p3BP8LFg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-nestjs-core@0.44.1': + resolution: {integrity: sha512-4TXaqJK27QXoMqrt4+hcQ6rKFd8B6V4JfrTJKnqBmWR1cbaqd/uwyl9yxhNH1JEkyo8GaBfdpBC4ZE4FuUhPmg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-net@0.43.1': + resolution: {integrity: sha512-TaMqP6tVx9/SxlY81dHlSyP5bWJIKq+K7vKfk4naB/LX4LBePPY3++1s0edpzH+RfwN+tEGVW9zTb9ci0up/lQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-pg@0.51.1': + resolution: {integrity: sha512-QxgjSrxyWZc7Vk+qGSfsejPVFL1AgAJdSBMYZdDUbwg730D09ub3PXScB9d04vIqPriZ+0dqzjmQx0yWKiCi2Q==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-pino@0.46.1': + resolution: {integrity: sha512-HB8gD/9CNAKlTV+mdZehnFC4tLUtQ7e+729oGq88e4WipxzZxmMYuRwZ2vzOA9/APtq+MRkERJ9PcoDqSIjZ+g==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-redis-4@0.46.1': + resolution: {integrity: sha512-UMqleEoabYMsWoTkqyt9WAzXwZ4BlFZHO40wr3d5ZvtjKCHlD4YXLm+6OLCeIi/HkX7EXvQaz8gtAwkwwSEvcQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-redis@0.46.1': + resolution: {integrity: sha512-AN7OvlGlXmlvsgbLHs6dS1bggp6Fcki+GxgYZdSrb/DB692TyfjR7sVILaCe0crnP66aJuXsg9cge3hptHs9UA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-restify@0.45.1': + resolution: {integrity: sha512-Zd6Go9iEa+0zcoA2vDka9r/plYKaT3BhD3ESIy4JNIzFWXeQBGbH3zZxQIsz0jbNTMEtonlymU7eTLeaGWiApA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-router@0.44.1': + resolution: {integrity: sha512-l4T/S7ByjpY5TCUPeDe1GPns02/5BpR0jroSMexyH3ZnXJt9PtYqx1IKAlOjaFEGEOQF2tGDsMi4PY5l+fSniQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-socket.io@0.46.1': + resolution: {integrity: sha512-9AsCVUAHOqvfe2RM/2I0DsDnx2ihw1d5jIN4+Bly1YPFTJIbk4+bXjAkr9+X6PUfhiV5urQHZkiYYPU1Q4yzPA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-tedious@0.18.1': + resolution: {integrity: sha512-5Cuy/nj0HBaH+ZJ4leuD7RjgvA844aY2WW+B5uLcWtxGjRZl3MNLuxnNg5DYWZNPO+NafSSnra0q49KWAHsKBg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-undici@0.10.1': + resolution: {integrity: sha512-rkOGikPEyRpMCmNu9AQuV5dtRlDmJp2dK5sw8roVshAGoB6hH/3QjDtRhdwd75SsJwgynWUNRUYe0wAkTo16tQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.7.0 + + '@opentelemetry/instrumentation-winston@0.44.1': + resolution: {integrity: sha512-iexblTsT3fP0hHUz/M1mWr+Ylg3bsYN2En/jvKXZtboW3Qkvt17HrQJYTF9leVIkXAfN97QxAcTE99YGbQW7vQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation@0.57.2': + resolution: {integrity: sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-exporter-base@0.57.2': + resolution: {integrity: sha512-XdxEzL23Urhidyebg5E6jZoaiW5ygP/mRjxLHixogbqwDy2Faduzb5N0o/Oi+XTIJu+iyxXdVORjXax+Qgfxag==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-grpc-exporter-base@0.57.2': + resolution: {integrity: sha512-USn173KTWy0saqqRB5yU9xUZ2xdgb1Rdu5IosJnm9aV4hMTuFFRTUsQxbgc24QxpCHeoKzzCSnS/JzdV0oM2iQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-transformer@0.57.2': + resolution: {integrity: sha512-48IIRj49gbQVK52jYsw70+Jv+JbahT8BqT2Th7C4H7RCM9d0gZ5sgNPoMpWldmfjvIsSgiGJtjfk9MeZvjhoig==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/propagation-utils@0.30.16': + resolution: {integrity: sha512-ZVQ3Z/PQ+2GQlrBfbMMMT0U7MzvYZLCPP800+ooyaBqm4hMvuQHfP028gB9/db0mwkmyEAMad9houukUVxhwcw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/propagator-b3@1.30.1': + resolution: {integrity: sha512-oATwWWDIJzybAZ4pO76ATN5N6FFbOA1otibAVlS8v90B4S1wClnhRUk7K+2CHAwN1JKYuj4jh/lpCEG5BAqFuQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/propagator-jaeger@1.30.1': + resolution: {integrity: sha512-Pj/BfnYEKIOImirH76M4hDaBSx6HyZ2CXUqk+Kj02m6BB80c/yo4BdWkn/1gDFfU+YPY+bPR2U0DKBfdxCKwmg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/redis-common@0.36.2': + resolution: {integrity: sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g==} + engines: {node: '>=14'} + + '@opentelemetry/resource-detector-alibaba-cloud@0.30.1': + resolution: {integrity: sha512-9l0FVP3F4+Z6ax27vMzkmhZdNtxAbDqEfy7rduzya3xFLaRiJSvOpw6cru6Edl5LwO+WvgNui+VzHa9ViE8wCg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resource-detector-aws@1.12.0': + resolution: {integrity: sha512-Cvi7ckOqiiuWlHBdA1IjS0ufr3sltex2Uws2RK6loVp4gzIJyOijsddAI6IZ5kiO8h/LgCWe8gxPmwkTKImd+Q==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resource-detector-azure@0.6.1': + resolution: {integrity: sha512-Djr31QCExVfWViaf9cGJnH+bUInD72p0GEfgDGgjCAztyvyji6WJvKjs6qmkpPN+Ig6KLk0ho2VgzT5Kdl4L2Q==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resource-detector-container@0.6.1': + resolution: {integrity: sha512-o4sLzx149DQXDmVa8pgjBDEEKOj9SuQnkSLbjUVOpQNnn10v0WNR6wLwh30mFsK26xOJ6SpqZBGKZiT7i5MjlA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resource-detector-gcp@0.33.1': + resolution: {integrity: sha512-/aZJXI1rU6Eus04ih2vU0hxXAibXXMzH1WlDZ8bXcTJmhwmTY8cP392+6l7cWeMnTQOibBUz8UKV3nhcCBAefw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resources@1.30.1': + resolution: {integrity: sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/sdk-logs@0.57.2': + resolution: {integrity: sha512-TXFHJ5c+BKggWbdEQ/inpgIzEmS2BGQowLE9UhsMd7YYlUfBQJ4uax0VF/B5NYigdM/75OoJGhAV3upEhK+3gg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + + '@opentelemetry/sdk-metrics@1.30.1': + resolution: {integrity: sha512-q9zcZ0Okl8jRgmy7eNW3Ku1XSgg3sDLa5evHZpCwjspw7E8Is4K/haRPDJrBcX3YSn/Y7gUvFnByNYEKQNbNog==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-node@0.57.2': + resolution: {integrity: sha512-8BaeqZyN5sTuPBtAoY+UtKwXBdqyuRKmekN5bFzAO40CgbGzAxfTpiL3PBerT7rhZ7p2nBdq7FaMv/tBQgHE4A==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@1.30.1': + resolution: {integrity: sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/sdk-trace-node@1.30.1': + resolution: {integrity: sha512-cBjYOINt1JxXdpw1e5MlHmFRc5fgj4GW/86vsKFxJCJ8AL4PdVtYH41gWwl4qd4uQjqEL1oJVrXkSy5cnduAnQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.28.0': + resolution: {integrity: sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==} + engines: {node: '>=14'} + '@opentelemetry/semantic-conventions@1.37.0': resolution: {integrity: sha512-JD6DerIKdJGmRp4jQyX5FlrQjA4tjOw1cvfsPAZXfOOEErMUHjPcPSICS+6WnM0nB0efSFARh0KAZss+bvExOA==} engines: {node: '>=14'} + '@opentelemetry/sql-common@0.40.1': + resolution: {integrity: sha512-nSDlnHSqzC3pXn/wZEZVLuAuJ1MYMXPBwtv2qAbCa3847SaHItdE7SzUq/Jtb0KZmh1zfAbNi3AAMjztTT4Ugg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -879,20 +1516,50 @@ packages: '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} - '@posthog/cli@0.4.8': - resolution: {integrity: sha512-LdPMD9vppl8TOMeG+sYsKDgs3dK8cA8vvgG65i0DrXlCPt2+lpRINtQJ+M7Bm8VZFUpzMieNOl+/o7axwxwXlw==} + '@posthog/cli@0.5.1': + resolution: {integrity: sha512-1a3zcLyP7fJp+IJ+jmodZ9poaQwGXJuz1/N9vEquudHqXi8cnhPy4OrbBoY3y1wZN4doYTwGqbfbts0662OUEw==} engines: {node: '>=14', npm: '>=6'} hasBin: true '@posthog/core@1.3.0': resolution: {integrity: sha512-hxLL8kZNHH098geedcxCz8y6xojkNYbmJEW+1vFXsmPcExyCXIUUJ/34X6xa9GcprKxd0Wsx3vfJQLQX4iVPhw==} - '@posthog/nextjs-config@1.3.3': - resolution: {integrity: sha512-kNtlD0DG1dGp9rgngRhGsLcRViduCgUmbz/huXQUjkH4FsbPzpUvXZzuYipVR3l+UREz074mNje+Y57jEWBMrQ==} + '@posthog/nextjs-config@1.3.4': + resolution: {integrity: sha512-cf1XIkK28y/5qL/as/IutH0EwysWy1gKdGM/xETZ0l25jTTN9wASx1ucP/BeE3dOYAVF039zlU9WNCFbIxW4Uw==} engines: {node: '>=20'} peerDependencies: next: '>12.1.0' + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@puppeteer/browsers@2.10.10': resolution: {integrity: sha512-3ZG500+ZeLql8rE0hjfhkycJjDj0pI/btEh3L9IkWUYcOrgP0xCNRq3HbtbqOPbvDhFaAWD88pDFtlLv8ns8gA==} engines: {node: '>=18'} @@ -1701,9 +2368,6 @@ packages: cpu: [x64] os: [win32] - '@sec-ant/readable-stream@0.4.1': - resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} - '@sparticuz/chromium@140.0.0': resolution: {integrity: sha512-pDyHiSp+buakpUq23b74JPC9T5M58665y6ULlh8uSuIDK0vxVGyLzjTTigQL202c6+0+NNp1Po5rgWcT7JSO5g==} engines: {node: '>=20.11.0'} @@ -1805,8 +2469,8 @@ packages: '@tailwindcss/postcss@4.1.14': resolution: {integrity: sha512-BdMjIxy7HUNThK87C7BC8I1rE8BVUsfNQSI5siQ4JK3iIa3w0XyVvVL9SXLWO//CtYTcp1v7zci0fYwJOjB+Zg==} - '@tanstack/query-core@5.90.3': - resolution: {integrity: sha512-HtPOnCwmx4dd35PfXU8jjkhwYrsHfuqgC8RCJIwWglmhIUIlzPP0ZcEkDAc+UtAWCiLm7T8rxeEfHZlz3hYMCA==} + '@tanstack/query-core@5.90.5': + resolution: {integrity: sha512-wLamYp7FaDq6ZnNehypKI5fNvxHPfTYylE0m/ZpuuzJfJqhR5Pxg9gvGBHZx4n7J+V5Rg5mZxHHTlv25Zt5u+w==} '@tanstack/query-devtools@5.90.1': resolution: {integrity: sha512-GtINOPjPUH0OegJExZ70UahT9ykmAhmtNVcmtdnOZbxLwT7R5OmRztR5Ahe3/Cu7LArEmR6/588tAycuaWb1xQ==} @@ -1817,8 +2481,8 @@ packages: '@tanstack/react-query': ^5.90.2 react: ^18 || ^19 - '@tanstack/react-query@5.90.3': - resolution: {integrity: sha512-i/LRL6DtuhG6bjGzavIMIVuKKPWx2AnEBIsBfuMm3YoHne0a20nWmsatOCBcVSaT0/8/5YFjNkebHAPLVUSi0Q==} + '@tanstack/react-query@5.90.5': + resolution: {integrity: sha512-pN+8UWpxZkEJ/Rnnj2v2Sxpx1WFlaa9L6a4UO89p6tTQbeo+m0MS8oYDjbggrR8QcTyjKoYWKS3xJQGr3ExT8Q==} peerDependencies: react: ^18 || ^19 @@ -1851,6 +2515,10 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' + '@tokenizer/inflate@0.2.7': + resolution: {integrity: sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==} + engines: {node: '>=18'} + '@tokenizer/token@0.3.0': resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} @@ -1881,6 +2549,9 @@ packages: '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + '@types/aws-lambda@8.10.147': + resolution: {integrity: sha512-nD0Z9fNIZcxYX5Mai2CTmFD7wX7UldCkW2ezCF8D1T5hdiLsnTWDGRpfRYntU6VjTdLQjOvyszru7I1c1oCQew==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -1893,9 +2564,18 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/bunyan@1.8.11': + resolution: {integrity: sha512-758fRH7umIMk5qt5ELmRMff4mLDlN+xyYzC+dkPTdKwbSkJFvz6xwyScrytPU0QIBbRRwbiE8/BIg8bpajerNQ==} + '@types/chai@5.2.2': resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} @@ -1911,12 +2591,33 @@ packages: '@types/mapbox__point-geometry@0.1.4': resolution: {integrity: sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==} - '@types/node@24.7.2': - resolution: {integrity: sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==} + '@types/memcached@2.2.10': + resolution: {integrity: sha512-AM9smvZN55Gzs2wRrqeMHVP7KE8KWgCJO/XL5yCly2xF6EKa4YlbpK+cLSAH4NG/Ah64HrlegmGqW8kYws7Vxg==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/mysql@2.15.26': + resolution: {integrity: sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==} + + '@types/node@22.18.11': + resolution: {integrity: sha512-Gd33J2XIrXurb+eT2ktze3rJAfAp9ZNjlBdh4SVgyrKEOADwCbdUDaK7QgJno8Ue4kcajscsKqu6n8OBG3hhCQ==} + + '@types/node@24.8.1': + resolution: {integrity: sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q==} '@types/pbf@3.0.5': resolution: {integrity: sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==} + '@types/pg-pool@2.0.6': + resolution: {integrity: sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==} + + '@types/pg@8.15.5': + resolution: {integrity: sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==} + + '@types/pg@8.6.1': + resolution: {integrity: sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==} + '@types/react-dom@19.1.9': resolution: {integrity: sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==} peerDependencies: @@ -1925,9 +2626,15 @@ packages: '@types/react@19.1.16': resolution: {integrity: sha512-WBM/nDbEZmDUORKnh5i1bTnAz6vTohUf9b8esSMu+b24+srbaxa04UbJgWx78CVfNXA20sNu0odEIluZDFdCog==} + '@types/shimmer@1.2.0': + resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==} + '@types/supercluster@7.1.3': resolution: {integrity: sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==} + '@types/tedious@4.0.14': + resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} + '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} @@ -1966,8 +2673,8 @@ packages: vue-router: optional: true - '@vercel/functions@3.1.3': - resolution: {integrity: sha512-dGYDSOfzjr5ZLfQ2bb4f+2NySsTLX4d6xnUr2Ckc5q/TrPTSJPLjzRzYLQAYO3/aeOv7evSYvPhijURmocu0UQ==} + '@vercel/functions@3.1.4': + resolution: {integrity: sha512-1dEfZkb7qxsA+ilo+1uBUCEgr7e90vHcimpDYkUB84DM051wQ5amJDk9x+cnaI29paZb5XukXwGl8yk3Udb/DQ==} engines: {node: '>= 20'} peerDependencies: '@aws-sdk/credential-provider-web-identity': '*' @@ -1975,8 +2682,8 @@ packages: '@aws-sdk/credential-provider-web-identity': optional: true - '@vercel/oidc@3.0.2': - resolution: {integrity: sha512-JekxQ0RApo4gS4un/iMGsIL1/k4KUBe3HmnGcDvzHuFBdQdudEJgTqcsJC7y6Ul4Yw5CeykgvQbX2XeEJd0+DA==} + '@vercel/oidc@3.0.3': + resolution: {integrity: sha512-yNEQvPcVrK9sIe637+I0jD6leluPxzwJKx/Haw6F4H77CdDsszUn5V3o96LPziXkSNE2B83+Z3mjqGKBK/R6Gg==} engines: {node: '>= 20'} '@vis.gl/react-mapbox@8.1.0': @@ -2052,10 +2759,24 @@ packages: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} + acorn-import-attributes@1.9.5: + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + ansi-regex@4.1.1: + resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} + engines: {node: '>=6'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -2109,8 +2830,8 @@ packages: resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} engines: {node: '>=4'} - ast-v8-to-istanbul@0.3.6: - resolution: {integrity: sha512-9tx1z/7OF/a8EdYL3FKoBhxLf3h3D8fXvuSj0HknsVeli2HE40qbNZxyFhMtnydaRiamwFu9zhb+BsJ5tVPehQ==} + ast-v8-to-istanbul@0.3.7: + resolution: {integrity: sha512-kr1Hy6YRZBkGQSb6puP+D6FQ59Cx4m0siYhAxygMCAgadiWQ6oxAxQXHOMvJx67SJ63jRoVIIg5eXzUbbct1ww==} asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -2143,8 +2864,8 @@ packages: bare-abort-controller: optional: true - bare-fs@4.4.10: - resolution: {integrity: sha512-arqVF+xX/rJHwrONZaSPhlzleT2gXwVs9rsAe1p1mIVwWZI2A76/raio+KwwxfWMO8oV9Wo90EaUkS2QwVmy4w==} + bare-fs@4.4.11: + resolution: {integrity: sha512-Bejmm9zRMvMTRoHS+2adgmXw1ANZnCNx+B5dgZpGwlP1E3x6Yuxea8RToddHUbWtVV0iUMWqsgZr8+jcgUI2SA==} engines: {bare: '>=1.16.0'} peerDependencies: bare-buffer: '*' @@ -2176,8 +2897,8 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.8.16: - resolution: {integrity: sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==} + baseline-browser-mapping@2.8.17: + resolution: {integrity: sha512-j5zJcx6golJYTG6c05LUZ3Z8Gi+M62zRT/ycz4Xq4iCOdpcxwg7ngEYD4KA0eWZC7U17qh/Smq8bYbACJ0ipBA==} hasBin: true basic-ftp@5.0.5: @@ -2187,6 +2908,9 @@ packages: bidi-js@1.0.3: resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + bmp-js@0.1.0: resolution: {integrity: sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==} @@ -2204,6 +2928,9 @@ packages: buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} @@ -2228,13 +2955,20 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - caniuse-lite@1.0.30001750: - resolution: {integrity: sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ==} + caniuse-lite@1.0.30001751: + resolution: {integrity: sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==} + + canonicalize@1.0.8: + resolution: {integrity: sha512-0CNTVCLZggSh7bc5VkX5WWPWO+cyZbNd07IHIsSXLia/eAq+r836hgk+8BKoEh7949Mda87VUOitx5OddVj64A==} chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + cheap-ruler@4.0.0: resolution: {integrity: sha512-0BJa8f4t141BYKQyn9NSQt1PguFQXMXwZiA5shfoaBYHAb2fFk2RAX+tiWMoQU+Agtzt3mdt0JtuyshAXqZ+Vw==} @@ -2258,6 +2992,9 @@ packages: peerDependencies: devtools-protocol: '*' + cjs-module-lexer@1.4.3: + resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} @@ -2319,6 +3056,9 @@ packages: country-flag-icons@1.5.21: resolution: {integrity: sha512-0KmU4oeiyAM+F+atzK99ghQDQJKxEY3tiDhnRraVFL4o65rZgrmrx7xKi0b+hxcVpcEpuUbu+KCC6TKTZQTDcA==} + cross-fetch@4.1.0: + resolution: {integrity: sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -2426,6 +3166,102 @@ packages: domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + drizzle-kit@0.31.5: + resolution: {integrity: sha512-+CHgPFzuoTQTt7cOYCV6MOw2w8vqEn/ap1yv4bpZOWL03u7rlVRQhUY0WYT3rHsgVTXwYQDZaSUJSQrMBUKuWg==} + hasBin: true + + drizzle-orm@0.44.6: + resolution: {integrity: sha512-uy6uarrrEOc9K1u5/uhBFJbdF5VJ5xQ/Yzbecw3eAYOunv5FDeYkR2m8iitocdHBOHbvorviKOW5GVw0U1j4LQ==} + peerDependencies: + '@aws-sdk/client-rds-data': '>=3' + '@cloudflare/workers-types': '>=4' + '@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.13' + '@prisma/client': '*' + '@tidbcloud/serverless': '*' + '@types/better-sqlite3': '*' + '@types/pg': '*' + '@types/sql.js': '*' + '@upstash/redis': '>=1.34.7' + '@vercel/postgres': '>=0.8.0' + '@xata.io/client': '*' + better-sqlite3: '>=7' + bun-types: '*' + expo-sqlite: '>=14.0.0' + gel: '>=2' + knex: '*' + kysely: '*' + mysql2: '>=2' + pg: '>=8' + postgres: '>=3' + prisma: '*' + 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/sql.js': + optional: true + '@upstash/redis': + optional: true + '@vercel/postgres': + optional: true + '@xata.io/client': + optional: true + better-sqlite3: + optional: true + bun-types: + optional: true + expo-sqlite: + optional: true + gel: + optional: true + knex: + optional: true + kysely: + optional: true + mysql2: + optional: true + pg: + optional: true + postgres: + optional: true + prisma: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -2442,8 +3278,8 @@ packages: effect@3.17.7: resolution: {integrity: sha512-dpt0ONUn3zzAuul6k4nC/coTTw27AL5nhkORXgTi6NfMPzqWYa1M05oKmOMTxpVSTKepqXVcW9vIwkuaaqx9zA==} - electron-to-chromium@1.5.235: - resolution: {integrity: sha512-i/7ntLFwOdoHY7sgjlTIDo4Sl8EdoTjWIaKinYOVfC6bOp71bmwenyZthWHcasxgHDNWbWxvG9M3Ia116zIaYQ==} + electron-to-chromium@1.5.237: + resolution: {integrity: sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -2495,8 +3331,18 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} - esbuild@0.25.10: - resolution: {integrity: sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==} + esbuild-register@3.6.0: + resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} + peerDependencies: + esbuild: '>=0.12 <1' + + esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.25.11: + resolution: {integrity: sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==} engines: {node: '>=18'} hasBin: true @@ -2551,6 +3397,9 @@ packages: resolution: {integrity: sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==} engines: {node: '>=0.10.0'} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + extract-zip@2.0.1: resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} engines: {node: '>= 10.17.0'} @@ -2585,9 +3434,9 @@ packages: resolution: {integrity: sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==} engines: {node: '>=10'} - file-type@19.6.0: - resolution: {integrity: sha512-VZR5I7k5wkD0HgFnMsq5hOsSc710MJMu5Nc5QYsbe38NN5iPV/XTObYLc/cpttRTf6lX538+5uO1ZQRhYibiZQ==} - engines: {node: '>=18'} + file-type@21.0.0: + resolution: {integrity: sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg==} + engines: {node: '>=20'} find-my-way-ts@0.1.6: resolution: {integrity: sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==} @@ -2612,6 +3461,9 @@ packages: resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} engines: {node: '>= 6'} + forwarded-parse@2.1.2: + resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} + framer-motion@12.23.24: resolution: {integrity: sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==} peerDependencies: @@ -2634,6 +3486,14 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + gaxios@6.7.1: + resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==} + engines: {node: '>=14'} + + gcp-metadata@6.1.1: + resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==} + engines: {node: '>=14'} + geist@1.5.1: resolution: {integrity: sha512-mAHZxIsL2o3ZITFaBVFBnwyDOw+zNLYum6A6nIjpzCGIO8QtC3V76XF2RnZTyLx1wlDTmMDy8jg3Ib52MIjGvQ==} peerDependencies: @@ -2666,9 +3526,8 @@ packages: resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} engines: {node: '>=8'} - get-stream@9.0.1: - resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} - engines: {node: '>=18'} + get-tsconfig@4.12.0: + resolution: {integrity: sha512-LScr2aNr2FbjAjZh2C6X6BxRx1/x+aTDExct/xyq2XKbYOiG5c0aK7pMsSuyc0brz3ibr/lbQiHD9jzt4lccJw==} get-uri@6.0.5: resolution: {integrity: sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==} @@ -2693,6 +3552,10 @@ packages: globrex@0.1.2: resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + google-logging-utils@0.0.2: + resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==} + engines: {node: '>=14'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -2715,6 +3578,9 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} + hash.js@1.1.7: + resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -2737,9 +3603,9 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} - icojs@0.19.5: - resolution: {integrity: sha512-4RnOvFpl7HNgUYLmmLKzhCk/DCNS81sXSC8ENp+mUonMHn3m9dsE+oQLnmoznjwr2bV9n1IapfSpdxopDzYBXA==} - engines: {node: '>=18.17.1'} + icojs@0.20.0: + resolution: {integrity: sha512-lYzM8Awqrtw9bH8y37een2Gispb6iQAf9pDOHmDdTsuyd2PMqTAL2hZW7kB16kYAPN0mrsjx+Zct4sI9ro8S7w==} + engines: {node: '>=20.19.4'} iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} @@ -2752,10 +3618,53 @@ packages: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} + import-in-the-middle@1.15.0: + resolution: {integrity: sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==} + indent-string@4.0.0: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + inngest@3.44.3: + resolution: {integrity: sha512-tCzFBCl47+Mt6sscMiKeN28N+FPCyF/3ntVAGxgmx5NjgXHjXxGeAyWhYMAYSU1Agk+R8f+jjR/uN2bOHTs3DA==} + engines: {node: '>=20'} + peerDependencies: + '@sveltejs/kit': '>=1.27.3' + '@vercel/node': '>=2.15.9' + aws-lambda: '>=1.0.7' + express: '>=4.19.2' + fastify: '>=4.21.0' + h3: '>=1.8.1' + hono: '>=4.2.7' + koa: '>=2.14.2' + next: '>=12.0.0' + typescript: '>=5.8.0' + zod: '>=3.24.0' + peerDependenciesMeta: + '@sveltejs/kit': + optional: true + '@vercel/node': + optional: true + aws-lambda: + optional: true + express: + optional: true + fastify: + optional: true + h3: + optional: true + hono: + optional: true + koa: + optional: true + next: + optional: true + typescript: + optional: true + ip-address@10.0.1: resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==} engines: {node: '>= 12'} @@ -2767,6 +3676,10 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + is-extendable@0.1.1: resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} engines: {node: '>=0.10.0'} @@ -2786,9 +3699,9 @@ packages: is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} - is-stream@4.0.1: - resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} - engines: {node: '>=18'} + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} is-what@4.1.16: resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} @@ -2858,12 +3771,18 @@ packages: engines: {node: '>=6'} hasBin: true + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} json-stringify-pretty-compact@3.0.0: resolution: {integrity: sha512-Rc2suX5meI0S3bfdZuA7JMFBGkJ875ApfVyq2WHELjBiiG22My/l7/8zPpH/CfFVQHuVLd8NLR0nv6vi0BYYKA==} + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -2939,6 +3858,12 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} @@ -2956,8 +3881,8 @@ packages: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} - lucide-react@0.545.0: - resolution: {integrity: sha512-7r1/yUuflQDSt4f1bpn5ZAocyIxcTyVyBBChSVtBKn5M+392cPmI5YJMWOJKk/HUWGm5wg83chlAZtCcGbEZtw==} + lucide-react@0.546.0: + resolution: {integrity: sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -3000,6 +3925,9 @@ packages: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} + minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + minimatch@10.0.3: resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==} engines: {node: 20 || >=22} @@ -3022,6 +3950,9 @@ packages: mitt@3.0.1: resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + module-details-from-path@1.0.4: + resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + motion-dom@12.23.23: resolution: {integrity: sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==} @@ -3111,8 +4042,8 @@ packages: resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} hasBin: true - node-releases@2.0.23: - resolution: {integrity: sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==} + node-releases@2.0.25: + resolution: {integrity: sha512-4auku8B/vw5psvTiiN9j1dAOsXvMoGqJuKJcR+dTdqiXEK20mMTk1UEo3HS16LeGQsVG6+qKTPM9u/qQ2LqATA==} nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} @@ -3152,6 +4083,9 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-scurry@1.11.1: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} @@ -3175,13 +4109,20 @@ packages: resolution: {integrity: sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==} engines: {node: '>=8'} - peek-readable@5.4.2: - resolution: {integrity: sha512-peBp3qZyuS6cNIJ2akRNG1uo1WJ1d0wTxg/fxMdZ0BqCVhx242bSFHM9eNqflfJVS9SsgkzgT/1UgnsurBOTMg==} - engines: {node: '>=14.16'} - pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-protocol@1.10.3: + resolution: {integrity: sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -3209,8 +4150,24 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} - posthog-js@1.275.2: - resolution: {integrity: sha512-g1fnV/GAcEdwwk4EVbJ1HMZhlhgKYxG1Z5KPGvr+q5re0ltyVq8jFA2PsF333jvOlI8R01LLdpYSIgU8sBiZfg==} + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.0: + resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + + posthog-js@1.276.0: + resolution: {integrity: sha512-FYZE1037LrAoKKeUU0pUL7u8WwNK2BVeg5TFApwquVPUdj9h7u5Z077A313hPN19Ar+7Y+VHxqYqdHc4VNsVgw==} peerDependencies: '@rrweb/types': 2.0.0-alpha.17 rrweb-snapshot: 2.0.0-alpha.17 @@ -3242,6 +4199,10 @@ packages: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} + protobufjs@7.5.4: + resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} + engines: {node: '>=12.0.0'} + protocol-buffers-schema@3.6.0: resolution: {integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==} @@ -3287,8 +4248,8 @@ packages: '@types/react-dom': optional: true - rdapper@0.7.0: - resolution: {integrity: sha512-J58VZJwPnroJASBWXJWyH+7BbzpR/tQ/lJKogbuz4JDbk+ehWI2jRbRycjVANkXuJKz9zmtVAkccfAJq2X6i1A==} + rdapper@0.8.0: + resolution: {integrity: sha512-jQ1I3l9GoUYoEbCxLZw70hrtff6pssSSt0lGeyidF2KUe+hfzIMyroerW3/heXjAVqJrNOQcNrb8+7OjDVv0qw==} engines: {node: '>=18.17'} hasBin: true @@ -3374,13 +4335,25 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + require-in-the-middle@7.5.2: + resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==} + engines: {node: '>=8.6.0'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve-protobuf-schema@2.1.0: resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==} + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + rimraf@6.0.1: resolution: {integrity: sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==} engines: {node: 20 || >=22} @@ -3422,6 +4395,10 @@ packages: engines: {node: '>=10'} hasBin: true + serialize-error-cjs@0.1.4: + resolution: {integrity: sha512-6a6dNqipzbCPlTFgztfNP2oG+IGcflMe/01zSzGrQcxGMKbIjOemBBD85pH92klWaJavAUWxAh9Z0aU28zxW6A==} + deprecated: Rolling release, please update to 0.2.0 + serialize-to-js@3.1.2: resolution: {integrity: sha512-owllqNuDDEimQat7EPG0tH7JjO090xKNzUtYz6X+Sk2BXDnOCilDdNLwjWeFywG9xkJul1ULvtUQa9O4pUaY0w==} engines: {node: '>=4.0.0'} @@ -3445,6 +4422,9 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shimmer@1.2.1: + resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -3490,6 +4470,9 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} @@ -3524,6 +4507,10 @@ packages: string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + strip-ansi@5.2.0: + resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==} + engines: {node: '>=6'} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -3539,14 +4526,14 @@ packages: strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + strtok3@10.3.4: + resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==} + engines: {node: '>=18'} + strtok3@6.3.0: resolution: {integrity: sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==} engines: {node: '>=10'} - strtok3@9.1.1: - resolution: {integrity: sha512-FhwotcEqjr241ZbjFzjlIYg6c5/L/s4yBGWSMvJ9UoExiSqL+FnFA/CaeZx17WGaZMS/4SOZp8wH18jSS4R4lw==} - engines: {node: '>=16'} - styled-jsx@5.1.6: resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} engines: {node: '>= 12.0.0'} @@ -3571,6 +4558,10 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -3594,6 +4585,12 @@ packages: resolution: {integrity: sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==} engines: {node: '>=18'} + temporal-polyfill@0.2.5: + resolution: {integrity: sha512-ye47xp8Cb0nDguAhrrDS1JT1SzwEV9e26sSsrWzVu+yPZ7LzceEcH0i2gci9jWfOfSCCgM3Qv5nOYShVUUFUXA==} + + temporal-spec@0.2.4: + resolution: {integrity: sha512-lDMFv4nKQrSjlkHKAlHVqKrBG4DyFfa9F74cmBZ3Iy3ed8yvWnlWSIdi4IKfSqwmazAohBNwiN64qGx4y5Q3IQ==} + test-exclude@7.0.1: resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} engines: {node: '>=18'} @@ -3682,6 +4679,11 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.20.6: + resolution: {integrity: sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==} + engines: {node: '>=18.0.0'} + hasBin: true + tunnel@0.0.6: resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} @@ -3710,6 +4712,9 @@ packages: uncrypto@0.1.3: resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.14.0: resolution: {integrity: sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==} @@ -3777,6 +4782,10 @@ packages: resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} hasBin: true + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + vaul@1.1.2: resolution: {integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==} peerDependencies: @@ -3942,6 +4951,10 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -3989,7 +5002,7 @@ snapshots: '@csstools/css-tokenizer': 3.0.4 lru-cache: 11.2.2 - '@asamuzakjp/dom-selector@6.6.2': + '@asamuzakjp/dom-selector@6.7.2': dependencies: '@asamuzakjp/nwsapi': 2.3.9 bidi-js: 1.0.3 @@ -4152,6 +5165,8 @@ snapshots: '@borewit/text-codec@0.1.1': {} + '@bufbuild/protobuf@2.9.0': {} + '@canvas/image-data@1.1.0': {} '@csstools/color-helpers@5.1.0': {} @@ -4180,6 +5195,8 @@ snapshots: '@date-fns/utc@2.1.1': {} + '@drizzle-team/brocli@0.10.2': {} + '@effect/platform@0.90.3(effect@3.17.7)': dependencies: '@opentelemetry/semantic-conventions': 1.37.0 @@ -4188,87 +5205,165 @@ snapshots: msgpackr: 1.11.5 multipasta: 0.2.7 + '@electric-sql/pglite@0.3.11': {} + '@emnapi/runtime@1.5.0': dependencies: tslib: 2.8.1 optional: true - '@esbuild/aix-ppc64@0.25.10': + '@esbuild-kit/core-utils@3.3.2': + dependencies: + esbuild: 0.18.20 + source-map-support: 0.5.21 + + '@esbuild-kit/esm-loader@2.6.5': + dependencies: + '@esbuild-kit/core-utils': 3.3.2 + get-tsconfig: 4.12.0 + + '@esbuild/aix-ppc64@0.25.11': optional: true - '@esbuild/android-arm64@0.25.10': + '@esbuild/android-arm64@0.18.20': optional: true - '@esbuild/android-arm@0.25.10': + '@esbuild/android-arm64@0.25.11': optional: true - '@esbuild/android-x64@0.25.10': + '@esbuild/android-arm@0.18.20': optional: true - '@esbuild/darwin-arm64@0.25.10': + '@esbuild/android-arm@0.25.11': optional: true - '@esbuild/darwin-x64@0.25.10': + '@esbuild/android-x64@0.18.20': optional: true - '@esbuild/freebsd-arm64@0.25.10': + '@esbuild/android-x64@0.25.11': optional: true - '@esbuild/freebsd-x64@0.25.10': + '@esbuild/darwin-arm64@0.18.20': optional: true - '@esbuild/linux-arm64@0.25.10': + '@esbuild/darwin-arm64@0.25.11': optional: true - '@esbuild/linux-arm@0.25.10': + '@esbuild/darwin-x64@0.18.20': optional: true - '@esbuild/linux-ia32@0.25.10': + '@esbuild/darwin-x64@0.25.11': optional: true - '@esbuild/linux-loong64@0.25.10': + '@esbuild/freebsd-arm64@0.18.20': optional: true - '@esbuild/linux-mips64el@0.25.10': + '@esbuild/freebsd-arm64@0.25.11': optional: true - '@esbuild/linux-ppc64@0.25.10': + '@esbuild/freebsd-x64@0.18.20': optional: true - '@esbuild/linux-riscv64@0.25.10': + '@esbuild/freebsd-x64@0.25.11': optional: true - '@esbuild/linux-s390x@0.25.10': + '@esbuild/linux-arm64@0.18.20': optional: true - '@esbuild/linux-x64@0.25.10': + '@esbuild/linux-arm64@0.25.11': optional: true - '@esbuild/netbsd-arm64@0.25.10': + '@esbuild/linux-arm@0.18.20': optional: true - '@esbuild/netbsd-x64@0.25.10': + '@esbuild/linux-arm@0.25.11': optional: true - '@esbuild/openbsd-arm64@0.25.10': + '@esbuild/linux-ia32@0.18.20': optional: true - '@esbuild/openbsd-x64@0.25.10': + '@esbuild/linux-ia32@0.25.11': optional: true - '@esbuild/openharmony-arm64@0.25.10': + '@esbuild/linux-loong64@0.18.20': optional: true - '@esbuild/sunos-x64@0.25.10': + '@esbuild/linux-loong64@0.25.11': optional: true - '@esbuild/win32-arm64@0.25.10': + '@esbuild/linux-mips64el@0.18.20': optional: true - '@esbuild/win32-ia32@0.25.10': + '@esbuild/linux-mips64el@0.25.11': optional: true - '@esbuild/win32-x64@0.25.10': + '@esbuild/linux-ppc64@0.18.20': + optional: true + + '@esbuild/linux-ppc64@0.25.11': + optional: true + + '@esbuild/linux-riscv64@0.18.20': + optional: true + + '@esbuild/linux-riscv64@0.25.11': + optional: true + + '@esbuild/linux-s390x@0.18.20': + optional: true + + '@esbuild/linux-s390x@0.25.11': + optional: true + + '@esbuild/linux-x64@0.18.20': + optional: true + + '@esbuild/linux-x64@0.25.11': + optional: true + + '@esbuild/netbsd-arm64@0.25.11': + optional: true + + '@esbuild/netbsd-x64@0.18.20': + optional: true + + '@esbuild/netbsd-x64@0.25.11': + optional: true + + '@esbuild/openbsd-arm64@0.25.11': + optional: true + + '@esbuild/openbsd-x64@0.18.20': + optional: true + + '@esbuild/openbsd-x64@0.25.11': + optional: true + + '@esbuild/openharmony-arm64@0.25.11': + optional: true + + '@esbuild/sunos-x64@0.18.20': + optional: true + + '@esbuild/sunos-x64@0.25.11': + optional: true + + '@esbuild/win32-arm64@0.18.20': + optional: true + + '@esbuild/win32-arm64@0.25.11': + optional: true + + '@esbuild/win32-ia32@0.18.20': + optional: true + + '@esbuild/win32-ia32@0.25.11': + optional: true + + '@esbuild/win32-x64@0.18.20': + optional: true + + '@esbuild/win32-x64@0.25.11': optional: true '@floating-ui/core@1.7.3': @@ -4288,6 +5383,18 @@ snapshots: '@floating-ui/utils@0.2.10': {} + '@grpc/grpc-js@1.14.0': + dependencies: + '@grpc/proto-loader': 0.8.0 + '@js-sdsl/ordered-map': 4.4.2 + + '@grpc/proto-loader@0.8.0': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.5.4 + yargs: 17.7.2 + '@img/colour@1.0.0': {} '@img/sharp-darwin-arm64@0.34.4': @@ -4376,6 +5483,11 @@ snapshots: '@img/sharp-win32-x64@0.34.4': optional: true + '@inngest/ai@0.1.7': + dependencies: + '@types/node': 22.18.11 + typescript: 5.9.3 + '@isaacs/balanced-match@4.0.1': {} '@isaacs/brace-expansion@5.0.0': @@ -4426,6 +5538,8 @@ snapshots: dependencies: regenerator-runtime: 0.13.11 + '@jpwilliams/waitgroup@2.1.1': {} + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -4445,6 +5559,8 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@js-sdsl/ordered-map@4.4.2': {} + '@mapbox/jsonlint-lines-primitives@2.0.2': {} '@mapbox/mapbox-gl-supported@3.0.0': {} @@ -4490,6 +5606,11 @@ snapshots: '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': optional: true + '@neondatabase/serverless@1.0.2': + dependencies: + '@types/node': 22.18.11 + '@types/pg': 8.15.5 + '@next/env@15.6.0-canary.39': {} '@next/swc-darwin-arm64@15.6.0-canary.39': @@ -4516,14 +5637,684 @@ snapshots: '@next/swc-win32-x64-msvc@15.6.0-canary.39': optional: true + '@opentelemetry/api-logs@0.57.2': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/api@1.9.0': {} + + '@opentelemetry/auto-instrumentations-node@0.56.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-amqplib': 0.46.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-aws-lambda': 0.50.3(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-aws-sdk': 0.49.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-bunyan': 0.45.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-cassandra-driver': 0.45.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-connect': 0.43.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-cucumber': 0.14.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-dataloader': 0.16.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-dns': 0.43.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-express': 0.47.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-fastify': 0.44.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-fs': 0.19.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-generic-pool': 0.43.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-graphql': 0.47.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-grpc': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-hapi': 0.45.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-http': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-ioredis': 0.47.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-kafkajs': 0.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-knex': 0.44.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-koa': 0.47.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-lru-memoizer': 0.44.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-memcached': 0.43.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mongodb': 0.52.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mongoose': 0.46.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mysql': 0.45.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mysql2': 0.45.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-nestjs-core': 0.44.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-net': 0.43.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-pg': 0.51.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-pino': 0.46.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-redis': 0.46.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-redis-4': 0.46.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-restify': 0.45.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-router': 0.44.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-socket.io': 0.46.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-tedious': 0.18.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-undici': 0.10.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-winston': 0.44.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-alibaba-cloud': 0.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-aws': 1.12.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-azure': 0.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-container': 0.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-gcp': 0.33.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-node': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - encoding + - supports-color + + '@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/exporter-logs-otlp-grpc@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.14.0 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-logs-otlp-http@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-logs-otlp-proto@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-metrics-otlp-grpc@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.14.0 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-metrics-otlp-http@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-metrics-otlp-proto@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-prometheus@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-trace-otlp-grpc@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.14.0 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-trace-otlp-http@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-trace-otlp-proto@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-zipkin@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/instrumentation-amqplib@0.46.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-aws-lambda@0.50.3(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + '@types/aws-lambda': 8.10.147 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-aws-sdk@0.49.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/propagation-utils': 0.30.16(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-bunyan@0.45.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@types/bunyan': 1.8.11 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-cassandra-driver@0.45.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-connect@0.43.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + '@types/connect': 3.4.38 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-cucumber@0.14.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-dataloader@0.16.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-dns@0.43.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-express@0.47.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-fastify@0.44.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-fs@0.19.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-generic-pool@0.43.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-graphql@0.47.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-grpc@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-hapi@0.45.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-http@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 + forwarded-parse: 2.1.2 + semver: 7.7.3 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-ioredis@0.47.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/redis-common': 0.36.2 + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-kafkajs@0.7.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-knex@0.44.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-koa@0.47.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-lru-memoizer@0.44.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-memcached@0.43.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + '@types/memcached': 2.2.10 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mongodb@0.52.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mongoose@0.46.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mysql2@0.45.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + '@opentelemetry/sql-common': 0.40.1(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mysql@0.45.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + '@types/mysql': 2.15.26 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-nestjs-core@0.44.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-net@0.43.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-pg@0.51.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + '@opentelemetry/sql-common': 0.40.1(@opentelemetry/api@1.9.0) + '@types/pg': 8.6.1 + '@types/pg-pool': 2.0.6 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-pino@0.46.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-redis-4@0.46.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/redis-common': 0.36.2 + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-redis@0.46.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/redis-common': 0.36.2 + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-restify@0.45.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-router@0.44.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-socket.io@0.46.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-tedious@0.18.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + '@types/tedious': 4.0.14 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-undici@0.10.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-winston@0.44.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.57.2 + '@types/shimmer': 1.2.0 + import-in-the-middle: 1.15.0 + require-in-the-middle: 7.5.2 + semver: 7.7.3 + shimmer: 1.2.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/otlp-exporter-base@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-grpc-exporter-base@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.14.0 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-transformer@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + protobufjs: 7.5.4 + + '@opentelemetry/propagation-utils@0.30.16(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/propagator-b3@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/propagator-jaeger@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/redis-common@0.36.2': {} + + '@opentelemetry/resource-detector-alibaba-cloud@0.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + + '@opentelemetry/resource-detector-aws@1.12.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + + '@opentelemetry/resource-detector-azure@0.6.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + + '@opentelemetry/resource-detector-container@0.6.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + + '@opentelemetry/resource-detector-gcp@0.33.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + gcp-metadata: 6.1.1 + transitivePeerDependencies: + - encoding + - supports-color + + '@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/sdk-logs@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-metrics@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-node@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-grpc': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-proto': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-grpc': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-proto': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-prometheus': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-grpc': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-proto': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-zipkin': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/sdk-trace-node@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-b3': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-jaeger': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + semver: 7.7.3 + + '@opentelemetry/semantic-conventions@1.28.0': {} + '@opentelemetry/semantic-conventions@1.37.0': {} + '@opentelemetry/sql-common@0.40.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@pkgjs/parseargs@0.11.0': optional: true '@polka/url@1.0.0-next.29': {} - '@posthog/cli@0.4.8': + '@posthog/cli@0.5.1': dependencies: axios: 1.12.2 axios-proxy-builder: 0.1.2 @@ -4535,15 +6326,38 @@ snapshots: '@posthog/core@1.3.0': {} - '@posthog/nextjs-config@1.3.3(next@15.6.0-canary.39(@babel/core@7.28.4)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))': + '@posthog/nextjs-config@1.3.4(next@15.6.0-canary.39(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))': dependencies: - '@posthog/cli': 0.4.8 + '@posthog/cli': 0.5.1 '@posthog/core': 1.3.0 - next: 15.6.0-canary.39(@babel/core@7.28.4)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + next: 15.6.0-canary.39(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) semver: 7.7.3 transitivePeerDependencies: - debug + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.4': {} + + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.0': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.0': {} + '@puppeteer/browsers@2.10.10': dependencies: debug: 4.4.3 @@ -5374,8 +7188,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.52.4': optional: true - '@sec-ant/readable-stream@0.4.1': {} - '@sparticuz/chromium@140.0.0': dependencies: follow-redirects: 1.15.11 @@ -5466,19 +7278,19 @@ snapshots: postcss: 8.5.6 tailwindcss: 4.1.14 - '@tanstack/query-core@5.90.3': {} + '@tanstack/query-core@5.90.5': {} '@tanstack/query-devtools@5.90.1': {} - '@tanstack/react-query-devtools@5.90.2(@tanstack/react-query@5.90.3(react@19.1.1))(react@19.1.1)': + '@tanstack/react-query-devtools@5.90.2(@tanstack/react-query@5.90.5(react@19.1.1))(react@19.1.1)': dependencies: '@tanstack/query-devtools': 5.90.1 - '@tanstack/react-query': 5.90.3(react@19.1.1) + '@tanstack/react-query': 5.90.5(react@19.1.1) react: 19.1.1 - '@tanstack/react-query@5.90.3(react@19.1.1)': + '@tanstack/react-query@5.90.5(react@19.1.1)': dependencies: - '@tanstack/query-core': 5.90.3 + '@tanstack/query-core': 5.90.5 react: 19.1.1 '@testing-library/dom@10.4.1': @@ -5515,6 +7327,14 @@ snapshots: dependencies: '@testing-library/dom': 10.4.1 + '@tokenizer/inflate@0.2.7': + dependencies: + debug: 4.4.3 + fflate: 0.8.2 + token-types: 6.1.1 + transitivePeerDependencies: + - supports-color + '@tokenizer/token@0.3.0': {} '@tootallnate/quickjs-emscripten@0.23.0': {} @@ -5528,9 +7348,9 @@ snapshots: dependencies: typescript: 5.9.3 - '@trpc/tanstack-react-query@11.6.0(@tanstack/react-query@5.90.3(react@19.1.1))(@trpc/client@11.6.0(@trpc/server@11.6.0(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.6.0(typescript@5.9.3))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.9.3)': + '@trpc/tanstack-react-query@11.6.0(@tanstack/react-query@5.90.5(react@19.1.1))(@trpc/client@11.6.0(@trpc/server@11.6.0(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.6.0(typescript@5.9.3))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.9.3)': dependencies: - '@tanstack/react-query': 5.90.3(react@19.1.1) + '@tanstack/react-query': 5.90.5(react@19.1.1) '@trpc/client': 11.6.0(@trpc/server@11.6.0(typescript@5.9.3))(typescript@5.9.3) '@trpc/server': 11.6.0(typescript@5.9.3) react: 19.1.1 @@ -5539,6 +7359,8 @@ snapshots: '@types/aria-query@5.0.4': {} + '@types/aws-lambda@8.10.147': {} + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.28.4 @@ -5560,10 +7382,22 @@ snapshots: dependencies: '@babel/types': 7.28.4 + '@types/bunyan@1.8.11': + dependencies: + '@types/node': 24.8.1 + '@types/chai@5.2.2': dependencies: '@types/deep-eql': 4.0.2 + '@types/connect@3.4.38': + dependencies: + '@types/node': 24.8.1 + + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 + '@types/deep-eql@4.0.2': {} '@types/estree@1.0.8': {} @@ -5576,12 +7410,42 @@ snapshots: '@types/mapbox__point-geometry@0.1.4': {} - '@types/node@24.7.2': + '@types/memcached@2.2.10': + dependencies: + '@types/node': 24.8.1 + + '@types/ms@2.1.0': {} + + '@types/mysql@2.15.26': + dependencies: + '@types/node': 24.8.1 + + '@types/node@22.18.11': + dependencies: + undici-types: 6.21.0 + + '@types/node@24.8.1': dependencies: undici-types: 7.14.0 '@types/pbf@3.0.5': {} + '@types/pg-pool@2.0.6': + dependencies: + '@types/pg': 8.6.1 + + '@types/pg@8.15.5': + dependencies: + '@types/node': 24.8.1 + pg-protocol: 1.10.3 + pg-types: 2.2.0 + + '@types/pg@8.6.1': + dependencies: + '@types/node': 24.8.1 + pg-protocol: 1.10.3 + pg-types: 2.2.0 + '@types/react-dom@19.1.9(@types/react@19.1.16)': dependencies: '@types/react': 19.1.16 @@ -5590,13 +7454,19 @@ snapshots: dependencies: csstype: 3.1.3 + '@types/shimmer@1.2.0': {} + '@types/supercluster@7.1.3': dependencies: '@types/geojson': 7946.0.16 + '@types/tedious@4.0.14': + dependencies: + '@types/node': 24.8.1 + '@types/yauzl@2.10.3': dependencies: - '@types/node': 24.7.2 + '@types/node': 24.8.1 optional: true '@uploadthing/mime-types@0.3.6': {} @@ -5611,16 +7481,16 @@ snapshots: dependencies: uncrypto: 0.1.3 - '@vercel/analytics@1.5.0(next@15.6.0-canary.39(@babel/core@7.28.4)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1)': + '@vercel/analytics@1.5.0(next@15.6.0-canary.39(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1)': optionalDependencies: - next: 15.6.0-canary.39(@babel/core@7.28.4)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + next: 15.6.0-canary.39(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) react: 19.1.1 - '@vercel/functions@3.1.3': + '@vercel/functions@3.1.4': dependencies: - '@vercel/oidc': 3.0.2 + '@vercel/oidc': 3.0.3 - '@vercel/oidc@3.0.2': {} + '@vercel/oidc@3.0.3': {} '@vis.gl/react-mapbox@8.1.0(mapbox-gl@3.15.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: @@ -5635,7 +7505,7 @@ snapshots: react: 19.1.1 react-dom: 19.1.1(react@19.1.1) - '@vitejs/plugin-react@5.0.4(vite@7.1.10(@types/node@24.7.2)(jiti@2.6.1)(lightningcss@1.30.1))': + '@vitejs/plugin-react@5.0.4(vite@7.1.10(@types/node@24.8.1)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6))': dependencies: '@babel/core': 7.28.4 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.4) @@ -5643,7 +7513,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.38 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 7.1.10(@types/node@24.7.2)(jiti@2.6.1)(lightningcss@1.30.1) + vite: 7.1.10(@types/node@24.8.1)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6) transitivePeerDependencies: - supports-color @@ -5651,7 +7521,7 @@ snapshots: dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 - ast-v8-to-istanbul: 0.3.6 + ast-v8-to-istanbul: 0.3.7 debug: 4.4.3 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 @@ -5662,7 +7532,7 @@ snapshots: std-env: 3.10.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@24.7.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.1) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.8.1)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.1)(tsx@4.20.6) transitivePeerDependencies: - supports-color @@ -5674,13 +7544,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@7.1.10(@types/node@24.7.2)(jiti@2.6.1)(lightningcss@1.30.1))': + '@vitest/mocker@3.2.4(vite@7.1.10(@types/node@24.8.1)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.19 optionalDependencies: - vite: 7.1.10(@types/node@24.7.2)(jiti@2.6.1)(lightningcss@1.30.1) + vite: 7.1.10(@types/node@24.8.1)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6) '@vitest/pretty-format@3.2.4': dependencies: @@ -5711,7 +7581,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@24.7.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.1) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.8.1)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.1)(tsx@4.20.6) '@vitest/utils@3.2.4': dependencies: @@ -5723,8 +7593,16 @@ snapshots: dependencies: event-target-shim: 5.0.1 + acorn-import-attributes@1.9.5(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + agent-base@7.1.4: {} + ansi-regex@4.1.1: {} + ansi-regex@5.0.1: {} ansi-regex@6.2.2: {} @@ -5761,7 +7639,7 @@ snapshots: dependencies: tslib: 2.8.1 - ast-v8-to-istanbul@0.3.6: + ast-v8-to-istanbul@0.3.7: dependencies: '@jridgewell/trace-mapping': 0.3.31 estree-walker: 3.0.3 @@ -5791,7 +7669,7 @@ snapshots: bare-events@2.8.0: {} - bare-fs@4.4.10: + bare-fs@4.4.11: dependencies: bare-events: 2.8.0 bare-path: 3.0.0 @@ -5828,7 +7706,7 @@ snapshots: base64-js@1.5.1: {} - baseline-browser-mapping@2.8.16: {} + baseline-browser-mapping@2.8.17: {} basic-ftp@5.0.5: {} @@ -5836,6 +7714,8 @@ snapshots: dependencies: require-from-string: 2.0.2 + bignumber.js@9.3.1: {} + bmp-js@0.1.0: {} boolbase@1.0.0: {} @@ -5846,14 +7726,16 @@ snapshots: browserslist@4.26.3: dependencies: - baseline-browser-mapping: 2.8.16 - caniuse-lite: 1.0.30001750 - electron-to-chromium: 1.5.235 - node-releases: 2.0.23 + baseline-browser-mapping: 2.8.17 + caniuse-lite: 1.0.30001751 + electron-to-chromium: 1.5.237 + node-releases: 2.0.25 update-browserslist-db: 1.1.3(browserslist@4.26.3) buffer-crc32@0.2.13: {} + buffer-from@1.1.2: {} + buffer@5.7.1: dependencies: base64-js: 1.5.1 @@ -5882,7 +7764,9 @@ snapshots: callsites@3.1.0: {} - caniuse-lite@1.0.30001750: {} + caniuse-lite@1.0.30001751: {} + + canonicalize@1.0.8: {} chai@5.3.3: dependencies: @@ -5892,6 +7776,11 @@ snapshots: loupe: 3.2.1 pathval: 2.0.1 + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + cheap-ruler@4.0.0: {} check-error@2.1.1: {} @@ -5927,6 +7816,8 @@ snapshots: mitt: 3.0.1 zod: 3.25.76 + cjs-module-lexer@1.4.3: {} + class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 @@ -5989,6 +7880,12 @@ snapshots: country-flag-icons@1.5.21: {} + cross-fetch@4.1.0: + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -6095,6 +7992,23 @@ snapshots: domelementtype: 2.3.0 domhandler: 5.0.3 + drizzle-kit@0.31.5: + dependencies: + '@drizzle-team/brocli': 0.10.2 + '@esbuild-kit/esm-loader': 2.6.5 + esbuild: 0.25.11 + esbuild-register: 3.6.0(esbuild@0.25.11) + transitivePeerDependencies: + - supports-color + + drizzle-orm@0.44.6(@electric-sql/pglite@0.3.11)(@neondatabase/serverless@1.0.2)(@opentelemetry/api@1.9.0)(@types/pg@8.15.5)(@upstash/redis@1.35.6): + optionalDependencies: + '@electric-sql/pglite': 0.3.11 + '@neondatabase/serverless': 1.0.2 + '@opentelemetry/api': 1.9.0 + '@types/pg': 8.15.5 + '@upstash/redis': 1.35.6 + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -6114,7 +8028,7 @@ snapshots: '@standard-schema/spec': 1.0.0 fast-check: 3.23.2 - electron-to-chromium@1.5.235: {} + electron-to-chromium@1.5.237: {} emoji-regex@8.0.0: {} @@ -6161,34 +8075,66 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.2 - esbuild@0.25.10: + esbuild-register@3.6.0(esbuild@0.25.11): + dependencies: + debug: 4.4.3 + esbuild: 0.25.11 + transitivePeerDependencies: + - supports-color + + esbuild@0.18.20: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.10 - '@esbuild/android-arm': 0.25.10 - '@esbuild/android-arm64': 0.25.10 - '@esbuild/android-x64': 0.25.10 - '@esbuild/darwin-arm64': 0.25.10 - '@esbuild/darwin-x64': 0.25.10 - '@esbuild/freebsd-arm64': 0.25.10 - '@esbuild/freebsd-x64': 0.25.10 - '@esbuild/linux-arm': 0.25.10 - '@esbuild/linux-arm64': 0.25.10 - '@esbuild/linux-ia32': 0.25.10 - '@esbuild/linux-loong64': 0.25.10 - '@esbuild/linux-mips64el': 0.25.10 - '@esbuild/linux-ppc64': 0.25.10 - '@esbuild/linux-riscv64': 0.25.10 - '@esbuild/linux-s390x': 0.25.10 - '@esbuild/linux-x64': 0.25.10 - '@esbuild/netbsd-arm64': 0.25.10 - '@esbuild/netbsd-x64': 0.25.10 - '@esbuild/openbsd-arm64': 0.25.10 - '@esbuild/openbsd-x64': 0.25.10 - '@esbuild/openharmony-arm64': 0.25.10 - '@esbuild/sunos-x64': 0.25.10 - '@esbuild/win32-arm64': 0.25.10 - '@esbuild/win32-ia32': 0.25.10 - '@esbuild/win32-x64': 0.25.10 + '@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 + + esbuild@0.25.11: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.11 + '@esbuild/android-arm': 0.25.11 + '@esbuild/android-arm64': 0.25.11 + '@esbuild/android-x64': 0.25.11 + '@esbuild/darwin-arm64': 0.25.11 + '@esbuild/darwin-x64': 0.25.11 + '@esbuild/freebsd-arm64': 0.25.11 + '@esbuild/freebsd-x64': 0.25.11 + '@esbuild/linux-arm': 0.25.11 + '@esbuild/linux-arm64': 0.25.11 + '@esbuild/linux-ia32': 0.25.11 + '@esbuild/linux-loong64': 0.25.11 + '@esbuild/linux-mips64el': 0.25.11 + '@esbuild/linux-ppc64': 0.25.11 + '@esbuild/linux-riscv64': 0.25.11 + '@esbuild/linux-s390x': 0.25.11 + '@esbuild/linux-x64': 0.25.11 + '@esbuild/netbsd-arm64': 0.25.11 + '@esbuild/netbsd-x64': 0.25.11 + '@esbuild/openbsd-arm64': 0.25.11 + '@esbuild/openbsd-x64': 0.25.11 + '@esbuild/openharmony-arm64': 0.25.11 + '@esbuild/sunos-x64': 0.25.11 + '@esbuild/win32-arm64': 0.25.11 + '@esbuild/win32-ia32': 0.25.11 + '@esbuild/win32-x64': 0.25.11 escalade@3.2.0: {} @@ -6233,6 +8179,8 @@ snapshots: assign-symbols: 1.0.0 is-extendable: 1.0.1 + extend@3.0.2: {} + extract-zip@2.0.1: dependencies: debug: 4.4.3 @@ -6267,12 +8215,14 @@ snapshots: strtok3: 6.3.0 token-types: 4.2.1 - file-type@19.6.0: + file-type@21.0.0: dependencies: - get-stream: 9.0.1 - strtok3: 9.1.1 + '@tokenizer/inflate': 0.2.7 + strtok3: 10.3.4 token-types: 6.1.1 uint8array-extras: 1.5.0 + transitivePeerDependencies: + - supports-color find-my-way-ts@0.1.6: {} @@ -6293,6 +8243,8 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 + forwarded-parse@2.1.2: {} + framer-motion@12.23.24(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: motion-dom: 12.23.23 @@ -6307,9 +8259,29 @@ snapshots: function-bind@1.1.2: {} - geist@1.5.1(next@15.6.0-canary.39(@babel/core@7.28.4)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)): + gaxios@6.7.1: dependencies: - next: 15.6.0-canary.39(@babel/core@7.28.4)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + extend: 3.0.2 + https-proxy-agent: 7.0.6 + is-stream: 2.0.1 + node-fetch: 2.7.0 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + gcp-metadata@6.1.1: + dependencies: + gaxios: 6.7.1 + google-logging-utils: 0.0.2 + json-bigint: 1.0.0 + transitivePeerDependencies: + - encoding + - supports-color + + geist@1.5.1(next@15.6.0-canary.39(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)): + dependencies: + next: 15.6.0-canary.39(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) gensync@1.0.0-beta.2: {} @@ -6341,10 +8313,9 @@ snapshots: dependencies: pump: 3.0.3 - get-stream@9.0.1: + get-tsconfig@4.12.0: dependencies: - '@sec-ant/readable-stream': 0.4.1 - is-stream: 4.0.1 + resolve-pkg-maps: 1.0.0 get-uri@6.0.5: dependencies: @@ -6378,6 +8349,8 @@ snapshots: globrex@0.1.2: {} + google-logging-utils@0.0.2: {} + gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -6392,6 +8365,11 @@ snapshots: dependencies: has-symbols: 1.1.0 + hash.js@1.1.7: + dependencies: + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -6423,16 +8401,17 @@ snapshots: transitivePeerDependencies: - supports-color - icojs@0.19.5(@jimp/custom@0.22.12): + icojs@0.20.0(@jimp/custom@0.22.12): dependencies: '@jimp/bmp': 0.22.12(@jimp/custom@0.22.12) decode-ico: 0.4.1 - file-type: 19.6.0 + file-type: 21.0.0 jpeg-js: 0.4.4 pngjs: 7.0.0 to-data-view: 2.0.0 transitivePeerDependencies: - '@jimp/custom' + - supports-color iconv-lite@0.6.3: dependencies: @@ -6445,14 +8424,59 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 + import-in-the-middle@1.15.0: + dependencies: + acorn: 8.15.0 + acorn-import-attributes: 1.9.5(acorn@8.15.0) + cjs-module-lexer: 1.4.3 + module-details-from-path: 1.0.4 + indent-string@4.0.0: {} + inherits@2.0.4: {} + + inngest@3.44.3(next@15.6.0-canary.39(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(typescript@5.9.3)(zod@4.1.12): + dependencies: + '@bufbuild/protobuf': 2.9.0 + '@inngest/ai': 0.1.7 + '@jpwilliams/waitgroup': 2.1.1 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/auto-instrumentations-node': 0.56.1(@opentelemetry/api@1.9.0) + '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + '@standard-schema/spec': 1.0.0 + '@types/debug': 4.1.12 + canonicalize: 1.0.8 + chalk: 4.1.2 + cross-fetch: 4.1.0 + debug: 4.4.3 + hash.js: 1.1.7 + json-stringify-safe: 5.0.1 + ms: 2.1.3 + serialize-error-cjs: 0.1.4 + strip-ansi: 5.2.0 + temporal-polyfill: 0.2.5 + zod: 4.1.12 + optionalDependencies: + next: 15.6.0-canary.39(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + typescript: 5.9.3 + transitivePeerDependencies: + - encoding + - supports-color + ip-address@10.0.1: {} ipaddr.js@2.2.0: {} is-arrayish@0.2.1: {} + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + is-extendable@0.1.1: {} is-extendable@1.0.1: @@ -6467,7 +8491,7 @@ snapshots: is-potential-custom-element-name@1.0.1: {} - is-stream@4.0.1: {} + is-stream@2.0.1: {} is-what@4.1.16: {} @@ -6527,7 +8551,7 @@ snapshots: jsdom@27.0.0(postcss@8.5.6): dependencies: - '@asamuzakjp/dom-selector': 6.6.2 + '@asamuzakjp/dom-selector': 6.7.2 cssstyle: 5.3.1(postcss@8.5.6) data-urls: 6.0.0 decimal.js: 10.6.0 @@ -6555,10 +8579,16 @@ snapshots: jsesc@3.1.0: {} + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.3.1 + json-parse-even-better-errors@2.3.1: {} json-stringify-pretty-compact@3.0.0: {} + json-stringify-safe@5.0.1: {} + json5@2.2.3: {} kdbush@4.0.2: {} @@ -6610,6 +8640,10 @@ snapshots: lines-and-columns@1.2.4: {} + lodash.camelcase@4.3.0: {} + + long@5.3.2: {} + loupe@3.2.1: {} lru-cache@10.4.3: {} @@ -6622,7 +8656,7 @@ snapshots: lru-cache@7.18.3: {} - lucide-react@0.545.0(react@19.1.1): + lucide-react@0.546.0(react@19.1.1): dependencies: react: 19.1.1 @@ -6690,6 +8724,8 @@ snapshots: min-indent@1.0.1: {} + minimalistic-assert@1.0.1: {} + minimatch@10.0.3: dependencies: '@isaacs/brace-expansion': 5.0.0 @@ -6708,6 +8744,8 @@ snapshots: mitt@3.0.1: {} + module-details-from-path@1.0.4: {} + motion-dom@12.23.23: dependencies: motion-utils: 12.23.6 @@ -6755,11 +8793,11 @@ snapshots: react: 19.1.1 react-dom: 19.1.1(react@19.1.1) - next@15.6.0-canary.39(@babel/core@7.28.4)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + next@15.6.0-canary.39(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: '@next/env': 15.6.0-canary.39 '@swc/helpers': 0.5.15 - caniuse-lite: 1.0.30001750 + caniuse-lite: 1.0.30001751 postcss: 8.4.31 react: 19.1.1 react-dom: 19.1.1(react@19.1.1) @@ -6773,6 +8811,7 @@ snapshots: '@next/swc-linux-x64-musl': 15.6.0-canary.39 '@next/swc-win32-arm64-msvc': 15.6.0-canary.39 '@next/swc-win32-x64-msvc': 15.6.0-canary.39 + '@opentelemetry/api': 1.9.0 babel-plugin-react-compiler: 19.1.0-rc.3 sharp: 0.34.4 transitivePeerDependencies: @@ -6788,7 +8827,7 @@ snapshots: detect-libc: 2.1.2 optional: true - node-releases@2.0.23: {} + node-releases@2.0.25: {} nth-check@2.1.1: dependencies: @@ -6844,6 +8883,8 @@ snapshots: path-key@3.1.1: {} + path-parse@1.0.7: {} + path-scurry@1.11.1: dependencies: lru-cache: 10.4.3 @@ -6864,10 +8905,20 @@ snapshots: peek-readable@4.1.0: {} - peek-readable@5.4.2: {} - pend@1.2.0: {} + pg-int8@1.0.1: {} + + pg-protocol@1.10.3: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.0 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + picocolors@1.1.1: {} picomatch@4.0.3: {} @@ -6892,7 +8943,17 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - posthog-js@1.275.2: + postgres-array@2.0.0: {} + + postgres-bytea@1.0.0: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + + posthog-js@1.276.0: dependencies: '@posthog/core': 1.3.0 core-js: 3.46.0 @@ -6918,6 +8979,21 @@ snapshots: progress@2.0.3: {} + protobufjs@7.5.4: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 24.8.1 + long: 5.3.2 + protocol-buffers-schema@3.6.0: {} proxy-agent@6.5.0: @@ -7043,7 +9119,7 @@ snapshots: '@types/react': 19.1.16 '@types/react-dom': 19.1.9(@types/react@19.1.16) - rdapper@0.7.0: + rdapper@0.8.0: dependencies: tldts: 7.0.17 @@ -7117,12 +9193,28 @@ snapshots: require-from-string@2.0.2: {} + require-in-the-middle@7.5.2: + dependencies: + debug: 4.4.3 + module-details-from-path: 1.0.4 + resolve: 1.22.10 + transitivePeerDependencies: + - supports-color + resolve-from@4.0.0: {} + resolve-pkg-maps@1.0.0: {} + resolve-protobuf-schema@2.1.0: dependencies: protocol-buffers-schema: 3.6.0 + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + rimraf@6.0.1: dependencies: glob: 11.0.3 @@ -7176,6 +9268,8 @@ snapshots: semver@7.7.3: {} + serialize-error-cjs@0.1.4: {} + serialize-to-js@3.1.2: {} server-only@0.0.1: {} @@ -7222,6 +9316,8 @@ snapshots: shebang-regex@3.0.0: {} + shimmer@1.2.1: {} + siginfo@2.0.0: {} signal-exit@4.1.0: {} @@ -7267,8 +9363,12 @@ snapshots: source-map-js@1.2.1: {} - source-map@0.6.1: - optional: true + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} splaytree@0.1.4: {} @@ -7307,6 +9407,10 @@ snapshots: dependencies: safe-buffer: 5.2.1 + strip-ansi@5.2.0: + dependencies: + ansi-regex: 4.1.1 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -7323,16 +9427,15 @@ snapshots: dependencies: js-tokens: 9.0.1 + strtok3@10.3.4: + dependencies: + '@tokenizer/token': 0.3.0 + strtok3@6.3.0: dependencies: '@tokenizer/token': 0.3.0 peek-readable: 4.1.0 - strtok3@9.1.1: - dependencies: - '@tokenizer/token': 0.3.0 - peek-readable: 5.4.2 - styled-jsx@5.1.6(@babel/core@7.28.4)(react@19.1.1): dependencies: client-only: 0.0.1 @@ -7352,6 +9455,8 @@ snapshots: dependencies: has-flag: 4.0.0 + supports-preserve-symlinks-flag@1.0.0: {} + symbol-tree@3.2.4: {} tailwind-merge@3.3.1: {} @@ -7365,7 +9470,7 @@ snapshots: pump: 3.0.3 tar-stream: 3.1.7 optionalDependencies: - bare-fs: 4.4.10 + bare-fs: 4.4.11 bare-path: 3.0.0 transitivePeerDependencies: - bare-abort-controller @@ -7389,6 +9494,12 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 + temporal-polyfill@0.2.5: + dependencies: + temporal-spec: 0.2.4 + + temporal-spec@0.2.4: {} + test-exclude@7.0.1: dependencies: '@istanbuljs/schema': 0.1.3 @@ -7461,6 +9572,13 @@ snapshots: tslib@2.8.1: {} + tsx@4.20.6: + dependencies: + esbuild: 0.25.11 + get-tsconfig: 4.12.0 + optionalDependencies: + fsevents: 2.3.3 + tunnel@0.0.6: {} tw-animate-css@1.4.0: {} @@ -7479,6 +9597,8 @@ snapshots: uncrypto@0.1.3: {} + undici-types@6.21.0: {} + undici-types@7.14.0: {} undici@7.16.0: {} @@ -7496,7 +9616,7 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 - uploadthing@7.7.4(next@15.6.0-canary.39(@babel/core@7.28.4)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(tailwindcss@4.1.14): + uploadthing@7.7.4(next@15.6.0-canary.39(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(tailwindcss@4.1.14): dependencies: '@effect/platform': 0.90.3(effect@3.17.7) '@standard-schema/spec': 1.0.0-beta.4 @@ -7504,7 +9624,7 @@ snapshots: '@uploadthing/shared': 7.1.10 effect: 3.17.7 optionalDependencies: - next: 15.6.0-canary.39(@babel/core@7.28.4)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + next: 15.6.0-canary.39(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) tailwindcss: 4.1.14 use-callback-ref@1.3.3(@types/react@19.1.16)(react@19.1.1): @@ -7528,6 +9648,8 @@ snapshots: uuid@13.0.0: {} + uuid@9.0.1: {} + vaul@1.1.2(@types/react-dom@19.1.9(@types/react@19.1.16))(@types/react@19.1.16)(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.1.9(@types/react@19.1.16))(@types/react@19.1.16)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -7537,13 +9659,13 @@ snapshots: - '@types/react' - '@types/react-dom' - vite-node@3.2.4(@types/node@24.7.2)(jiti@2.6.1)(lightningcss@1.30.1): + vite-node@3.2.4(@types/node@24.8.1)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.1.10(@types/node@24.7.2)(jiti@2.6.1)(lightningcss@1.30.1) + vite: 7.1.10(@types/node@24.8.1)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6) transitivePeerDependencies: - '@types/node' - jiti @@ -7558,36 +9680,37 @@ snapshots: - tsx - yaml - vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@7.1.10(@types/node@24.7.2)(jiti@2.6.1)(lightningcss@1.30.1)): + vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@7.1.10(@types/node@24.8.1)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)): dependencies: debug: 4.4.3 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.3) optionalDependencies: - vite: 7.1.10(@types/node@24.7.2)(jiti@2.6.1)(lightningcss@1.30.1) + vite: 7.1.10(@types/node@24.8.1)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6) transitivePeerDependencies: - supports-color - typescript - vite@7.1.10(@types/node@24.7.2)(jiti@2.6.1)(lightningcss@1.30.1): + vite@7.1.10(@types/node@24.8.1)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6): dependencies: - esbuild: 0.25.10 + esbuild: 0.25.11 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 rollup: 4.52.4 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 24.7.2 + '@types/node': 24.8.1 fsevents: 2.3.3 jiti: 2.6.1 lightningcss: 1.30.1 + tsx: 4.20.6 - vitest@3.2.4(@types/node@24.7.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.1): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.8.1)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.1)(tsx@4.20.6): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.1.10(@types/node@24.7.2)(jiti@2.6.1)(lightningcss@1.30.1)) + '@vitest/mocker': 3.2.4(vite@7.1.10(@types/node@24.8.1)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -7605,11 +9728,12 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.1.10(@types/node@24.7.2)(jiti@2.6.1)(lightningcss@1.30.1) - vite-node: 3.2.4(@types/node@24.7.2)(jiti@2.6.1)(lightningcss@1.30.1) + vite: 7.1.10(@types/node@24.8.1)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6) + vite-node: 3.2.4(@types/node@24.8.1)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 24.7.2 + '@types/debug': 4.1.12 + '@types/node': 24.8.1 '@vitest/ui': 3.2.4(vitest@3.2.4) jsdom: 27.0.0(postcss@8.5.6) transitivePeerDependencies: @@ -7690,6 +9814,8 @@ snapshots: xmlchars@2.2.0: {} + xtend@4.0.2: {} + y18n@5.0.8: {} yallist@3.1.1: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b1f09de..759e818 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,5 +4,6 @@ onlyBuiltDependencies: - core-js - esbuild - msgpackr-extract + - protobufjs - puppeteer - sharp diff --git a/server/db/client.ts b/server/db/client.ts new file mode 100644 index 0000000..2e8209d --- /dev/null +++ b/server/db/client.ts @@ -0,0 +1,12 @@ +import { Pool } from "@neondatabase/serverless"; +import { drizzle } from "drizzle-orm/neon-serverless"; +import * as schema from "@/server/db/schema"; + +const connectionString = process.env.DATABASE_URL; +if (!connectionString) { + // Throw at import time so we fail fast on misconfiguration in server-only context + throw new Error("DATABASE_URL is not set"); +} + +const pool = new Pool({ connectionString }); +export const db = drizzle(pool, { schema }); diff --git a/server/db/pglite.ts b/server/db/pglite.ts new file mode 100644 index 0000000..6c3680f --- /dev/null +++ b/server/db/pglite.ts @@ -0,0 +1,20 @@ +import { PGlite } from "@electric-sql/pglite"; +import { drizzle } from "drizzle-orm/pglite"; +import * as schema from "@/server/db/schema"; + +// Dynamic import via require pattern is recommended in community examples +// to access drizzle-kit/api in Vitest. +const { pushSchema } = + require("drizzle-kit/api") as typeof import("drizzle-kit/api"); + +export async function makePGliteDb() { + const client = new PGlite(); + const db = drizzle(client, { schema }); + const { apply } = await pushSchema( + schema, + // biome-ignore lint/suspicious/noExplicitAny: ignore type mismatch + db as any, + ); + await apply(); + return { db, client }; +} diff --git a/server/db/schema.ts b/server/db/schema.ts new file mode 100644 index 0000000..2b84606 --- /dev/null +++ b/server/db/schema.ts @@ -0,0 +1,267 @@ +import { sql } from "drizzle-orm"; +import { + boolean, + check, + doublePrecision, + index, + integer, + jsonb, + pgEnum, + pgTable, + text, + timestamp, + unique, + uuid, +} from "drizzle-orm/pg-core"; +import type { Registration } from "@/lib/schemas"; + +// Enums +export const providerCategory = pgEnum("provider_category", [ + "hosting", + "email", + "dns", + "ca", + "registrar", +]); +export const dnsRecordType = pgEnum("dns_record_type", [ + "A", + "AAAA", + "MX", + "TXT", + "NS", +]); +export const dnsResolver = pgEnum("dns_resolver", ["cloudflare", "google"]); + +// Providers +export const providers = pgTable( + "providers", + { + id: uuid("id").primaryKey().defaultRandom(), + category: providerCategory("category").notNull(), + name: text("name").notNull(), + domain: text("domain"), + slug: text("slug").notNull(), + createdAt: timestamp("created_at", { withTimezone: true }) + .defaultNow() + .notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }) + .defaultNow() + .notNull(), + }, + (t) => [unique("u_providers_category_slug").on(t.category, t.slug)], +); + +// Domains +export const domains = pgTable( + "domains", + { + id: uuid("id").primaryKey().defaultRandom(), + name: text("name").notNull(), + tld: text("tld").notNull(), + unicodeName: text("unicode_name").notNull(), + createdAt: timestamp("created_at", { withTimezone: true }) + .defaultNow() + .notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }) + .defaultNow() + .notNull(), + }, + (t) => [ + unique("u_domains_name").on(t.name), + index("i_domains_tld").on(t.tld), + ], +); + +// Registration (snapshot) +export const registrations = pgTable( + "registrations", + { + domainId: uuid("domain_id") + .primaryKey() + .references(() => domains.id, { onDelete: "cascade" }), + isRegistered: boolean("is_registered").notNull(), + privacyEnabled: boolean("privacy_enabled"), + registry: text("registry"), + creationDate: timestamp("creation_date", { withTimezone: true }), + updatedDate: timestamp("updated_date", { withTimezone: true }), + expirationDate: timestamp("expiration_date", { withTimezone: true }), + deletionDate: timestamp("deletion_date", { withTimezone: true }), + transferLock: boolean("transfer_lock"), + statuses: jsonb("statuses") + .$type() + .notNull() + .default(sql`'[]'::jsonb`), + contacts: jsonb("contacts") + .$type<{ contacts?: Registration["contacts"] }>() + .notNull() + .default(sql`'{}'::jsonb`), + whoisServer: text("whois_server"), + rdapServers: jsonb("rdap_servers") + .$type() + .notNull() + .default(sql`'[]'::jsonb`), + source: text("source").notNull(), + registrarProviderId: uuid("registrar_provider_id").references( + () => providers.id, + ), + resellerProviderId: uuid("reseller_provider_id").references( + () => providers.id, + ), + fetchedAt: timestamp("fetched_at", { withTimezone: true }).notNull(), + expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(), + }, + (t) => [ + index("i_reg_registrar").on(t.registrarProviderId), + index("i_reg_expires").on(t.expiresAt), + ], +); + +export const registrationNameservers = pgTable( + "registration_nameservers", + { + id: uuid("id").primaryKey().defaultRandom(), + domainId: uuid("domain_id") + .notNull() + .references(() => domains.id, { onDelete: "cascade" }), + host: text("host").notNull(), + ipv4: jsonb("ipv4").$type().notNull().default(sql`'[]'::jsonb`), + ipv6: jsonb("ipv6").$type().notNull().default(sql`'[]'::jsonb`), + }, + (t) => [ + unique("u_reg_ns").on(t.domainId, t.host), + index("i_reg_ns_host").on(t.host), + ], +); + +// DNS (per-record rows) +export const dnsRecords = pgTable( + "dns_records", + { + id: uuid("id").primaryKey().defaultRandom(), + domainId: uuid("domain_id") + .notNull() + .references(() => domains.id, { onDelete: "cascade" }), + type: dnsRecordType("type").notNull(), + name: text("name").notNull(), + value: text("value").notNull(), + ttl: integer("ttl"), + priority: integer("priority"), + isCloudflare: boolean("is_cloudflare"), + resolver: dnsResolver("resolver").notNull(), + fetchedAt: timestamp("fetched_at", { withTimezone: true }).notNull(), + expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(), + }, + (t) => [ + unique("u_dns_record").on(t.domainId, t.type, t.name, t.value), + index("i_dns_domain_type").on(t.domainId, t.type), + index("i_dns_type_value").on(t.type, t.value), + index("i_dns_expires").on(t.expiresAt), + ], +); + +// TLS certificates (latest) +export const certificates = pgTable( + "certificates", + { + id: uuid("id").primaryKey().defaultRandom(), + domainId: uuid("domain_id") + .notNull() + .references(() => domains.id, { onDelete: "cascade" }), + issuer: text("issuer").notNull(), + subject: text("subject").notNull(), + altNames: jsonb("alt_names").notNull().default(sql`'[]'::jsonb`), + validFrom: timestamp("valid_from", { withTimezone: true }).notNull(), + validTo: timestamp("valid_to", { withTimezone: true }).notNull(), + caProviderId: uuid("ca_provider_id").references(() => providers.id), + fetchedAt: timestamp("fetched_at", { withTimezone: true }).notNull(), + expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(), + }, + (t) => [ + index("i_certs_domain").on(t.domainId), + index("i_certs_valid_to").on(t.validTo), + index("i_certs_expires").on(t.expiresAt), + // Ensure validTo >= validFrom + check("ck_cert_valid_window", sql`${t.validTo} >= ${t.validFrom}`), + // GIN on alt_names via raw migration + ], +); + +// HTTP headers (latest set) +export const httpHeaders = pgTable( + "http_headers", + { + id: uuid("id").primaryKey().defaultRandom(), + domainId: uuid("domain_id") + .notNull() + .references(() => domains.id, { onDelete: "cascade" }), + name: text("name").notNull(), + value: text("value").notNull(), + fetchedAt: timestamp("fetched_at", { withTimezone: true }).notNull(), + expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(), + }, + (t) => [ + unique("u_http_header").on(t.domainId, t.name), + index("i_http_name").on(t.name), + ], +); + +// Hosting (latest) +export const hosting = pgTable( + "hosting", + { + domainId: uuid("domain_id") + .primaryKey() + .references(() => domains.id, { onDelete: "cascade" }), + hostingProviderId: uuid("hosting_provider_id").references( + () => providers.id, + ), + emailProviderId: uuid("email_provider_id").references(() => providers.id), + dnsProviderId: uuid("dns_provider_id").references(() => providers.id), + geoCity: text("geo_city"), + geoRegion: text("geo_region"), + geoCountry: text("geo_country"), + geoCountryCode: text("geo_country_code"), + geoLat: doublePrecision("geo_lat"), + geoLon: doublePrecision("geo_lon"), + fetchedAt: timestamp("fetched_at", { withTimezone: true }).notNull(), + expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(), + }, + (t) => [ + index("i_hosting_providers").on( + t.hostingProviderId, + t.emailProviderId, + t.dnsProviderId, + ), + ], +); + +// SEO (latest) +export const seo = pgTable( + "seo", + { + domainId: uuid("domain_id") + .primaryKey() + .references(() => domains.id, { onDelete: "cascade" }), + sourceFinalUrl: text("source_final_url"), + sourceStatus: integer("source_status"), + metaOpenGraph: jsonb("meta_open_graph").notNull().default(sql`'{}'::jsonb`), + metaTwitter: jsonb("meta_twitter").notNull().default(sql`'{}'::jsonb`), + metaGeneral: jsonb("meta_general").notNull().default(sql`'{}'::jsonb`), + previewTitle: text("preview_title"), + previewDescription: text("preview_description"), + previewImageUrl: text("preview_image_url"), + previewImageUploadedUrl: text("preview_image_uploaded_url"), + canonicalUrl: text("canonical_url"), + robots: jsonb("robots").notNull().default(sql`'{}'::jsonb`), + robotsSitemaps: jsonb("robots_sitemaps") + .notNull() + .default(sql`'[]'::jsonb`), + errors: jsonb("errors").notNull().default(sql`'[]'::jsonb`), + fetchedAt: timestamp("fetched_at", { withTimezone: true }).notNull(), + expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(), + }, + (t) => [ + index("i_seo_src_final_url").on(t.sourceFinalUrl), + index("i_seo_canonical").on(t.canonicalUrl), + ], +); diff --git a/server/db/seed/providers.ts b/server/db/seed/providers.ts new file mode 100644 index 0000000..df7ddbc --- /dev/null +++ b/server/db/seed/providers.ts @@ -0,0 +1,61 @@ +import { CA_PROVIDERS } from "@/lib/providers/rules/certificate"; +import { DNS_PROVIDERS } from "@/lib/providers/rules/dns"; +import { EMAIL_PROVIDERS } from "@/lib/providers/rules/email"; +import { HOSTING_PROVIDERS } from "@/lib/providers/rules/hosting"; +import { REGISTRAR_PROVIDERS } from "@/lib/providers/rules/registrar"; +import { db } from "@/server/db/client"; +import { type providerCategory, providers } from "@/server/db/schema"; + +function slugify(input: string): string { + return input + .trim() + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/(^-|-$)+/g, ""); +} + +type SeedDef = { + name: string; + domain: string | null; + category: (typeof providerCategory.enumValues)[number]; + aliases?: string[]; +}; + +function collect(): SeedDef[] { + const arr: SeedDef[] = []; + const push = ( + cat: SeedDef["category"], + src: { name: string; domain: string }[], + ) => { + for (const p of src) + arr.push({ name: p.name, domain: p.domain ?? null, category: cat }); + }; + push("dns", DNS_PROVIDERS); + push("email", EMAIL_PROVIDERS); + push("hosting", HOSTING_PROVIDERS); + push("registrar", REGISTRAR_PROVIDERS); + push("ca", CA_PROVIDERS); + return arr; +} + +async function main() { + const defs = collect(); + for (const def of defs) { + const slug = slugify(def.name); + await db + .insert(providers) + .values({ + name: def.name, + domain: def.domain ?? undefined, + category: def.category, + slug, + }) + .onConflictDoNothing({ target: [providers.category, providers.slug] }); + } + console.log(`Seeded ${defs.length} provider rows (existing skipped).`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/server/db/ttl.test.ts b/server/db/ttl.test.ts new file mode 100644 index 0000000..03b8eeb --- /dev/null +++ b/server/db/ttl.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from "vitest"; +import { + ttlForCertificates, + ttlForDnsRecord, + ttlForRegistration, +} from "@/server/db/ttl"; + +describe("TTL policy", () => { + it("registration: 24h when registered and far from expiry", () => { + const now = new Date("2024-01-01T00:00:00.000Z"); + const exp = new Date("2024-02-01T00:00:00.000Z"); + const d = ttlForRegistration(now, true, exp); + expect(d.getTime() - now.getTime()).toBe(24 * 60 * 60 * 1000); + }); + + it("registration: 6h when unregistered", () => { + const now = new Date("2024-01-01T00:00:00.000Z"); + const d = ttlForRegistration(now, false, null); + expect(d.getTime() - now.getTime()).toBe(6 * 60 * 60 * 1000); + }); + + it("registration: <=1h when expiry within 7d", () => { + const now = new Date("2024-01-01T00:00:00.000Z"); + const exp = new Date("2024-01-05T00:00:00.000Z"); + const d = ttlForRegistration(now, true, exp); + expect(d.getTime() - now.getTime()).toBe(60 * 60 * 1000); + }); + + it("dns: default 1h when ttl missing", () => { + const now = new Date("2024-01-01T00:00:00.000Z"); + const d = ttlForDnsRecord(now, undefined); + expect(d.getTime() - now.getTime()).toBe(60 * 60 * 1000); + }); + + it("dns: cap at 24h", () => { + const now = new Date("2024-01-01T00:00:00.000Z"); + const d = ttlForDnsRecord(now, 3 * 24 * 60 * 60); + expect(d.getTime() - now.getTime()).toBe(24 * 60 * 60 * 1000); + }); + + it("certs: before valid_to and within 24h window", () => { + const now = new Date("2024-01-01T00:00:00.000Z"); + const validTo = new Date("2024-01-04T00:00:00.000Z"); + const d = ttlForCertificates(now, validTo); + // min(now+24h, valid_to-48h) => valid_to-48h here (Jan 2) + expect(d.toISOString()).toBe( + new Date("2024-01-02T00:00:00.000Z").toISOString(), + ); + }); +}); diff --git a/server/db/ttl.ts b/server/db/ttl.ts new file mode 100644 index 0000000..0f5a374 --- /dev/null +++ b/server/db/ttl.ts @@ -0,0 +1,55 @@ +export function addSeconds(base: Date, seconds: number): Date { + return new Date(base.getTime() + seconds * 1000); +} + +export function clampFuture(min: Date, max: Date): Date { + return new Date( + Math.min(Math.max(min.getTime(), Date.now() + 60_000), max.getTime()), + ); +} + +export function ttlForRegistration( + now: Date, + isRegistered: boolean, + expirationDate?: Date | null, +): Date { + if (expirationDate) { + const msUntil = expirationDate.getTime() - now.getTime(); + if (msUntil <= 7 * 24 * 60 * 60 * 1000) { + // Revalidate more aggressively near expiry + return addSeconds(now, 60 * 60); // 1h + } + } + return addSeconds(now, isRegistered ? 24 * 60 * 60 : 6 * 60 * 60); +} + +export function ttlForDnsRecord(now: Date, ttlSeconds?: number | null): Date { + const ttl = + typeof ttlSeconds === "number" && ttlSeconds > 0 + ? Math.min(ttlSeconds, 24 * 60 * 60) + : 60 * 60; + return addSeconds(now, ttl); +} + +export function ttlForCertificates(now: Date, validTo: Date): Date { + // Revalidate certificates within a 24h sliding window, but start checking + // more aggressively 48h before expiry to catch upcoming expirations. + const window = addSeconds(now, 24 * 60 * 60); + const revalidateBefore = new Date(validTo.getTime() - 48 * 60 * 60 * 1000); + return clampFuture( + addSeconds(now, 60 * 60), + new Date(Math.min(window.getTime(), revalidateBefore.getTime())), + ); +} + +export function ttlForHeaders(now: Date): Date { + return addSeconds(now, 12 * 60 * 60); +} + +export function ttlForHosting(now: Date): Date { + return addSeconds(now, 24 * 60 * 60); +} + +export function ttlForSeo(now: Date): Date { + return addSeconds(now, 24 * 60 * 60); +} diff --git a/server/inngest/client.ts b/server/inngest/client.ts new file mode 100644 index 0000000..15f3a9a --- /dev/null +++ b/server/inngest/client.ts @@ -0,0 +1,4 @@ +import "server-only"; +import { Inngest } from "inngest"; + +export const inngest = new Inngest({ id: "hoot-app" }); diff --git a/server/inngest/functions/domain-inspected.ts b/server/inngest/functions/domain-inspected.ts new file mode 100644 index 0000000..6fd88f1 --- /dev/null +++ b/server/inngest/functions/domain-inspected.ts @@ -0,0 +1,29 @@ +import "server-only"; +import { type Section, SectionEnum } from "@/lib/schemas"; +import { inngest } from "@/server/inngest/client"; + +export const domainInspected = inngest.createFunction( + { id: "domain-inspected" }, + { event: "domain/inspected" }, + async ({ step, event }) => { + const { domain, sections: rawSections } = event.data as { + domain: string; + sections?: string[]; + }; + // Validate and filter sections + const sections: Section[] = rawSections + ? rawSections.filter((s): s is Section => + SectionEnum.options.includes(s as Section), + ) + : []; + for (const section of sections) { + await step.sendEvent("enqueue-section", { + name: "section/revalidate", + data: { + domain: typeof domain === "string" ? domain.trim().toLowerCase() : "", + section, + }, + }); + } + }, +); diff --git a/server/inngest/functions/scan-due.test.ts b/server/inngest/functions/scan-due.test.ts new file mode 100644 index 0000000..a594526 --- /dev/null +++ b/server/inngest/functions/scan-due.test.ts @@ -0,0 +1,17 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +describe("scan-due", () => { + beforeEach(async () => { + vi.resetModules(); + const { makePGliteDb } = await import("@/server/db/pglite"); + const { db } = await makePGliteDb(); + vi.doMock("@/server/db/client", () => ({ db })); + globalThis.__redisTestHelper.reset(); + }); + + it("counts due dns rows via db mock", async () => { + const { countDueDns } = await import("@/server/inngest/functions/scan-due"); + const n = await countDueDns(new Date()); + expect(typeof n).toBe("number"); + }); +}); diff --git a/server/inngest/functions/scan-due.ts b/server/inngest/functions/scan-due.ts new file mode 100644 index 0000000..0185475 --- /dev/null +++ b/server/inngest/functions/scan-due.ts @@ -0,0 +1,147 @@ +import "server-only"; +import { eq, lte } from "drizzle-orm"; +import { db } from "@/server/db/client"; +import { + certificates, + dnsRecords, + domains, + hosting, + httpHeaders, + registrations, + seo, +} from "@/server/db/schema"; +import { inngest } from "@/server/inngest/client"; + +export const scanDue = inngest.createFunction( + { id: "scan-due-revalidations" }, + { cron: "*/1 * * * *" }, + async ({ step }) => { + const now = new Date(); + const limit = 200; + + // Fetch due rows with error handling so failures surface with context + let dueDns: Array<{ domainId: string; domain: string }>; // dns + let dueHeaders: Array<{ domainId: string; domain: string }>; // headers + let dueHosting: Array<{ domainId: string; domain: string }>; // hosting + let dueCerts: Array<{ domainId: string; domain: string }>; // certificates + let dueSeo: Array<{ domainId: string; domain: string }>; // seo + let dueReg: Array<{ domainId: string; domain: string }>; // registration + try { + [dueDns, dueHeaders, dueHosting, dueCerts, dueSeo, dueReg] = + await Promise.all([ + db + .select({ domainId: dnsRecords.domainId, domain: domains.name }) + .from(dnsRecords) + .innerJoin(domains, eq(dnsRecords.domainId, domains.id)) + .where(lte(dnsRecords.expiresAt, now)) + .limit(limit), + db + .select({ domainId: httpHeaders.domainId, domain: domains.name }) + .from(httpHeaders) + .innerJoin(domains, eq(httpHeaders.domainId, domains.id)) + .where(lte(httpHeaders.expiresAt, now)) + .limit(limit), + db + .select({ domainId: hosting.domainId, domain: domains.name }) + .from(hosting) + .innerJoin(domains, eq(hosting.domainId, domains.id)) + .where(lte(hosting.expiresAt, now)) + .limit(limit), + db + .select({ domainId: certificates.domainId, domain: domains.name }) + .from(certificates) + .innerJoin(domains, eq(certificates.domainId, domains.id)) + .where(lte(certificates.expiresAt, now)) + .limit(limit), + db + .select({ domainId: seo.domainId, domain: domains.name }) + .from(seo) + .innerJoin(domains, eq(seo.domainId, domains.id)) + .where(lte(seo.expiresAt, now)) + .limit(limit), + db + .select({ domainId: registrations.domainId, domain: domains.name }) + .from(registrations) + .innerJoin(domains, eq(registrations.domainId, domains.id)) + .where(lte(registrations.expiresAt, now)) + .limit(limit), + ]); + } catch (error) { + console.error("[scan-due] database queries failed", { + error, + now, + limit, + }); + throw error; + } + + // Group sections per domain to deduplicate events + const domainsToSections = new Map>(); + const addSection = ( + domainName: string, + _domainId: string, + section: string, + ) => { + if (!domainName) return; + const key = domainName; + const set = domainsToSections.get(key) ?? new Set(); + set.add(section); + domainsToSections.set(key, set); + }; + + for (const r of dueReg) addSection(r.domain, r.domainId, "registration"); + for (const r of dueDns) addSection(r.domain, r.domainId, "dns"); + for (const r of dueHeaders) addSection(r.domain, r.domainId, "headers"); + for (const r of dueHosting) addSection(r.domain, r.domainId, "hosting"); + for (const r of dueCerts) addSection(r.domain, r.domainId, "certificates"); + for (const r of dueSeo) addSection(r.domain, r.domainId, "seo"); + + // Enforce a small payload: cap sections per domain (there are <=6 today) + const MAX_SECTIONS_PER_DOMAIN = 6; + const groupedEvents: Array<{ + name: string; + data: { domain: string; sections: string[] }; + }> = Array.from(domainsToSections.entries()).map(([domain, sections]) => ({ + name: "section/revalidate", + data: { + domain, + sections: Array.from(sections).slice(0, MAX_SECTIONS_PER_DOMAIN), + }, + })); + + if (groupedEvents.length === 0) { + return; + } + + // Batch events to avoid oversized payloads + const BATCH_SIZE = 200; + for (let i = 0; i < groupedEvents.length; i += BATCH_SIZE) { + const chunk = groupedEvents.slice(i, i + BATCH_SIZE); + try { + await step.sendEvent( + `enqueue-due-${Math.floor(i / BATCH_SIZE)}`, + chunk, + ); + } catch (error) { + console.error("[scan-due] sendEvent failed", { + error, + batchSize: chunk.length, + batchIndex: Math.floor(i / BATCH_SIZE), + }); + throw error; + } + } + }, +); + +export async function countDueDns( + now: Date = new Date(), + limit = 200, +): Promise { + const rows = await db + .select({ domainId: dnsRecords.domainId }) + .from(dnsRecords) + .where(lte(dnsRecords.expiresAt, now)) + .limit(limit); + return rows.length; +} diff --git a/server/inngest/functions/section-revalidate.test.ts b/server/inngest/functions/section-revalidate.test.ts new file mode 100644 index 0000000..35eae1c --- /dev/null +++ b/server/inngest/functions/section-revalidate.test.ts @@ -0,0 +1,111 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +// Import dns lazily inside tests to avoid module-scope DB client init + +describe("section-revalidate", () => { + beforeEach(async () => { + vi.resetModules(); + const { makePGliteDb } = await import("@/server/db/pglite"); + const { db } = await makePGliteDb(); + vi.doMock("@/server/db/client", () => ({ db })); + globalThis.__redisTestHelper.reset(); + }); + + it("calls dns resolver for dns section", async () => { + const { revalidateSection } = await import( + "@/server/inngest/functions/section-revalidate" + ); + const dnsMod = await import("@/server/services/dns"); + const spy = vi + .spyOn(dnsMod, "resolveAll") + .mockResolvedValue({ records: [], resolver: "cloudflare" }); + await revalidateSection("example.com", "dns"); + expect(spy).toHaveBeenCalledWith("example.com"); + }); + + it("invokes headers probe", async () => { + const { revalidateSection } = await import( + "@/server/inngest/functions/section-revalidate" + ); + const mod = await import("@/server/services/headers"); + const spy = vi.spyOn(mod, "probeHeaders").mockResolvedValue([]); + await revalidateSection("example.com", "headers"); + expect(spy).toHaveBeenCalledWith("example.com"); + }); + + it("invokes hosting detect", async () => { + const { revalidateSection } = await import( + "@/server/inngest/functions/section-revalidate" + ); + const mod = await import("@/server/services/hosting"); + const spy = vi.spyOn(mod, "detectHosting").mockResolvedValue({ + hostingProvider: { name: "", domain: null }, + emailProvider: { name: "", domain: null }, + dnsProvider: { name: "", domain: null }, + geo: { + city: "", + region: "", + country: "", + country_code: "", + lat: null, + lon: null, + }, + }); + await revalidateSection("example.com", "hosting"); + expect(spy).toHaveBeenCalledWith("example.com"); + }); + + it("invokes certificates fetch", async () => { + const { revalidateSection } = await import( + "@/server/inngest/functions/section-revalidate" + ); + const mod = await import("@/server/services/certificates"); + const spy = vi.spyOn(mod, "getCertificates").mockResolvedValue([]); + await revalidateSection("example.com", "certificates"); + expect(spy).toHaveBeenCalledWith("example.com"); + }); + + it("invokes seo fetch", async () => { + const { revalidateSection } = await import( + "@/server/inngest/functions/section-revalidate" + ); + const mod = await import("@/server/services/seo"); + const spy = vi.spyOn(mod, "getSeo").mockResolvedValue({ + meta: null, + robots: null, + preview: null, + source: { finalUrl: null, status: null }, + }); + await revalidateSection("example.com", "seo"); + expect(spy).toHaveBeenCalledWith("example.com"); + }); + + it("invokes registration lookup", async () => { + const { revalidateSection } = await import( + "@/server/inngest/functions/section-revalidate" + ); + const mod = await import("@/server/services/registration"); + const spy = vi.spyOn(mod, "getRegistration").mockResolvedValue({ + domain: "example.com", + tld: "com", + isRegistered: true, + registry: undefined, + creationDate: undefined, + updatedDate: undefined, + expirationDate: undefined, + deletionDate: undefined, + transferLock: undefined, + statuses: [], + contacts: [], + whoisServer: undefined, + rdapServers: [], + source: "rdap", + registrar: undefined, + reseller: undefined, + nameservers: [], + registrarProvider: { name: "", domain: null }, + }); + await revalidateSection("example.com", "registration"); + expect(spy).toHaveBeenCalledWith("example.com"); + }); +}); diff --git a/server/inngest/functions/section-revalidate.ts b/server/inngest/functions/section-revalidate.ts new file mode 100644 index 0000000..9be2c8b --- /dev/null +++ b/server/inngest/functions/section-revalidate.ts @@ -0,0 +1,94 @@ +import "server-only"; +import { z } from "zod"; +import { acquireLockOrWaitForResult } from "@/lib/cache"; +import { ns, redis } from "@/lib/redis"; +import { type Section, SectionEnum } from "@/lib/schemas"; +import { inngest } from "@/server/inngest/client"; +import { getCertificates } from "@/server/services/certificates"; +import { resolveAll } from "@/server/services/dns"; +import { probeHeaders } from "@/server/services/headers"; +import { detectHosting } from "@/server/services/hosting"; +import { getRegistration } from "@/server/services/registration"; +import { getSeo } from "@/server/services/seo"; + +const eventDataSchema = z.object({ + domain: z.string().min(1), + section: SectionEnum.optional(), + sections: z.array(SectionEnum).optional(), +}); + +export async function revalidateSection( + domain: string, + section: Section, +): Promise { + switch (section) { + case "dns": + await resolveAll(domain); + return; + case "headers": + await probeHeaders(domain); + return; + case "hosting": + await detectHosting(domain); + return; + case "certificates": + await getCertificates(domain); + return; + case "seo": + await getSeo(domain); + return; + case "registration": + await getRegistration(domain); + return; + } +} + +export const sectionRevalidate = inngest.createFunction( + { + id: "section-revalidate", + concurrency: { + key: "event.data.domain", + limit: 1, + }, + }, + { event: "section/revalidate" }, + async ({ event }) => { + const data = eventDataSchema.parse(event.data); + const domain = data.domain; + const normalizedDomain = + typeof domain === "string" ? domain.trim().toLowerCase() : ""; + + const sections: Section[] = Array.isArray(data.sections) + ? data.sections + : data.section + ? [data.section] + : []; + + if (sections.length === 0) return; + + for (const section of sections) { + const lockKey = ns("lock", "revalidate", section, normalizedDomain); + const resultKey = ns("result", "revalidate", section, normalizedDomain); + const wait = await acquireLockOrWaitForResult({ + lockKey, + resultKey, + lockTtl: 60, + }); + if (!wait.acquired) continue; + try { + await revalidateSection(normalizedDomain, section); + try { + await redis.set( + resultKey, + JSON.stringify({ completedAt: Date.now() }), + { ex: 55 }, + ); + } catch {} + } finally { + try { + await redis.del(lockKey); + } catch {} + } + } + }, +); diff --git a/server/repos/certificates.ts b/server/repos/certificates.ts new file mode 100644 index 0000000..264983a --- /dev/null +++ b/server/repos/certificates.ts @@ -0,0 +1,39 @@ +import "server-only"; +import type { InferInsertModel } from "drizzle-orm"; +import { eq } from "drizzle-orm"; +import { db } from "@/server/db/client"; +import { certificates } from "@/server/db/schema"; + +type CertificateInsert = InferInsertModel; + +export type UpsertCertificatesParams = { + domainId: string; + chain: Array< + Omit + >; + fetchedAt: Date; + expiresAt: Date; // policy window for revalidation (not cert validity) +}; + +export async function replaceCertificates(params: UpsertCertificatesParams) { + const { domainId } = params; + // Atomic delete and bulk insert in a single transaction + await db.transaction(async (tx) => { + await tx.delete(certificates).where(eq(certificates.domainId, domainId)); + if (params.chain.length > 0) { + await tx.insert(certificates).values( + params.chain.map((c) => ({ + domainId, + issuer: c.issuer, + subject: c.subject, + altNames: c.altNames, + validFrom: c.validFrom, + validTo: c.validTo, + caProviderId: c.caProviderId ?? null, + fetchedAt: params.fetchedAt, + expiresAt: params.expiresAt, + })), + ); + } + }); +} diff --git a/server/repos/dns.ts b/server/repos/dns.ts new file mode 100644 index 0000000..557ec62 --- /dev/null +++ b/server/repos/dns.ts @@ -0,0 +1,98 @@ +import "server-only"; +import type { InferInsertModel } from "drizzle-orm"; +import { and, eq, inArray } from "drizzle-orm"; +import { db } from "@/server/db/client"; +import { + dnsRecords, + type dnsRecordType, + type dnsResolver, +} from "@/server/db/schema"; + +type DnsRecordInsert = InferInsertModel; + +export type UpsertDnsParams = { + domainId: string; + resolver: (typeof dnsResolver.enumValues)[number]; + fetchedAt: Date; + // complete set per type + recordsByType: Record< + (typeof dnsRecordType.enumValues)[number], + Array< + Omit< + DnsRecordInsert, + "id" | "domainId" | "type" | "resolver" | "fetchedAt" + > + > + >; +}; + +export async function replaceDns(params: UpsertDnsParams) { + const { domainId, recordsByType } = params; + // For each type, compute replace-set by (type,name,value) + for (const type of Object.keys(recordsByType) as Array< + (typeof dnsRecordType.enumValues)[number] + >) { + const next = (recordsByType[type] ?? []).map((r) => ({ + ...r, + // Normalize DNS record name/value for case-insensitive uniqueness + name: (r.name as string).trim().toLowerCase(), + value: (r.value as string).trim().toLowerCase(), + })); + const existing = await db + .select({ + id: dnsRecords.id, + name: dnsRecords.name, + value: dnsRecords.value, + }) + .from(dnsRecords) + .where(and(eq(dnsRecords.domainId, domainId), eq(dnsRecords.type, type))); + const nextKey = (r: (typeof next)[number]) => + `${type as string}|${r.name as string}|${r.value as string}`; + const nextMap = new Map(next.map((r) => [nextKey(r), r])); + const toDelete = existing + .filter( + (e) => + !nextMap.has( + `${type}|${e.name.trim().toLowerCase()}|${e.value + .trim() + .toLowerCase()}`, + ), + ) + .map((e) => e.id); + if (toDelete.length > 0) { + await db.delete(dnsRecords).where(inArray(dnsRecords.id, toDelete)); + } + for (const r of next) { + await db + .insert(dnsRecords) + .values({ + domainId, + type, + name: r.name as string, + value: r.value as string, + ttl: r.ttl ?? null, + priority: r.priority ?? null, + isCloudflare: r.isCloudflare ?? null, + resolver: params.resolver, + fetchedAt: params.fetchedAt, + expiresAt: r.expiresAt as Date, + }) + .onConflictDoUpdate({ + target: [ + dnsRecords.domainId, + dnsRecords.type, + dnsRecords.name, + dnsRecords.value, + ], + set: { + ttl: r.ttl ?? null, + priority: r.priority ?? null, + isCloudflare: r.isCloudflare ?? null, + resolver: params.resolver, + fetchedAt: params.fetchedAt, + expiresAt: r.expiresAt as Date, + }, + }); + } + } +} diff --git a/server/repos/domains.ts b/server/repos/domains.ts new file mode 100644 index 0000000..4a80240 --- /dev/null +++ b/server/repos/domains.ts @@ -0,0 +1,37 @@ +import "server-only"; +import { eq } from "drizzle-orm"; +import { db } from "@/server/db/client"; +import { domains } from "@/server/db/schema"; + +export type UpsertDomainParams = { + name: string; // punycode lowercased + tld: string; + unicodeName: string; +}; + +export async function upsertDomain(params: UpsertDomainParams) { + const { name, tld, unicodeName } = params; + + const inserted = await db + .insert(domains) + .values({ name, tld, unicodeName }) + .onConflictDoNothing({ target: [domains.name] }) + .returning(); + if (inserted[0]) return inserted[0]; + + const rows = await db + .select() + .from(domains) + .where(eq(domains.name, name)) + .limit(1); + return rows[0]; +} + +export async function findDomainByName(name: string) { + const rows = await db + .select() + .from(domains) + .where(eq(domains.name, name)) + .limit(1); + return rows[0] ?? null; +} diff --git a/server/repos/headers.ts b/server/repos/headers.ts new file mode 100644 index 0000000..61f4534 --- /dev/null +++ b/server/repos/headers.ts @@ -0,0 +1,45 @@ +import "server-only"; +import { eq, inArray } from "drizzle-orm"; +import { db } from "@/server/db/client"; +import { httpHeaders } from "@/server/db/schema"; + +export type ReplaceHeadersParams = { + domainId: string; + headers: Array<{ name: string; value: string }>; + fetchedAt: Date; + expiresAt: Date; +}; + +export async function replaceHeaders(params: ReplaceHeadersParams) { + const { domainId, headers, fetchedAt, expiresAt } = params; + const existing = await db + .select({ id: httpHeaders.id, name: httpHeaders.name }) + .from(httpHeaders) + .where(eq(httpHeaders.domainId, domainId)); + // Normalize incoming header names (trim + lowercase) for maps and DB writes + const normalized = headers.map((h) => ({ + name: h.name.trim().toLowerCase(), + value: h.value, + })); + const nextByName = new Map(normalized.map((h) => [h.name, h])); + const toDelete = existing + .filter((e) => { + const normalizedName = e.name.trim().toLowerCase(); + const existsNext = nextByName.has(normalizedName); + const needsCaseNormalization = e.name !== normalizedName; + return !existsNext || needsCaseNormalization; + }) + .map((e) => e.id); + if (toDelete.length > 0) { + await db.delete(httpHeaders).where(inArray(httpHeaders.id, toDelete)); + } + for (const h of normalized) { + await db + .insert(httpHeaders) + .values({ domainId, name: h.name, value: h.value, fetchedAt, expiresAt }) + .onConflictDoUpdate({ + target: [httpHeaders.domainId, httpHeaders.name], + set: { value: h.value, fetchedAt, expiresAt }, + }); + } +} diff --git a/server/repos/hosting.ts b/server/repos/hosting.ts new file mode 100644 index 0000000..0d26d3e --- /dev/null +++ b/server/repos/hosting.ts @@ -0,0 +1,13 @@ +import "server-only"; +import type { InferInsertModel } from "drizzle-orm"; +import { db } from "@/server/db/client"; +import { hosting as hostingTable } from "@/server/db/schema"; + +type HostingInsert = InferInsertModel; + +export async function upsertHosting(params: HostingInsert) { + await db.insert(hostingTable).values(params).onConflictDoUpdate({ + target: hostingTable.domainId, + set: params, + }); +} diff --git a/server/repos/providers.ts b/server/repos/providers.ts new file mode 100644 index 0000000..aa41aa8 --- /dev/null +++ b/server/repos/providers.ts @@ -0,0 +1,46 @@ +import "server-only"; +import { and, eq, sql } from "drizzle-orm"; +import { db } from "@/server/db/client"; +import { type providerCategory, providers } from "@/server/db/schema"; + +export type ResolveProviderInput = { + category: (typeof providerCategory.enumValues)[number]; + domain?: string | null; + name?: string | null; +}; + +/** + * Resolve a provider id by exact domain when provided, falling back to case-insensitive name. + */ +export async function resolveProviderId( + input: ResolveProviderInput, +): Promise { + const { category } = input; + const domain = input.domain?.toLowerCase() ?? null; + const name = input.name?.trim() ?? null; + + if (domain) { + const byDomain = await db + .select({ id: providers.id }) + .from(providers) + .where( + and(eq(providers.category, category), eq(providers.domain, domain)), + ) + .limit(1); + if (byDomain[0]?.id) return byDomain[0].id; + } + if (name) { + const byName = await db + .select({ id: providers.id }) + .from(providers) + .where( + and( + eq(providers.category, category), + sql`lower(${providers.name}) = lower(${name})`, + ), + ) + .limit(1); + if (byName[0]?.id) return byName[0].id; + } + return null; +} diff --git a/server/repos/registrations.ts b/server/repos/registrations.ts new file mode 100644 index 0000000..e9f89a0 --- /dev/null +++ b/server/repos/registrations.ts @@ -0,0 +1,72 @@ +import "server-only"; +import type { InferInsertModel } from "drizzle-orm"; +import { eq, inArray } from "drizzle-orm"; +import { db } from "@/server/db/client"; +import { registrationNameservers, registrations } from "@/server/db/schema"; + +type RegistrationInsert = InferInsertModel; +type RegistrationNameserverInsert = InferInsertModel< + typeof registrationNameservers +>; + +export async function upsertRegistration( + params: RegistrationInsert & { + nameservers?: Array< + Pick + >; + }, +) { + const { domainId, nameservers: ns, ...rest } = params; + await db.transaction(async (tx) => { + await tx + .insert(registrations) + .values({ domainId, ...rest }) + .onConflictDoUpdate({ + target: registrations.domainId, + set: { ...rest }, + }); + + if (!ns) return; + // Replace-set semantics for nameservers + const existing = await tx + .select({ + id: registrationNameservers.id, + host: registrationNameservers.host, + }) + .from(registrationNameservers) + .where(eq(registrationNameservers.domainId, domainId)); + + const nextByHost = new Map(ns.map((n) => [n.host.trim().toLowerCase(), n])); + const toDelete = existing + .filter((e) => !nextByHost.has(e.host.toLowerCase())) + .map((e) => e.id); + + if (toDelete.length > 0) { + await tx + .delete(registrationNameservers) + .where(inArray(registrationNameservers.id, toDelete)); + } + + for (const n of ns) { + const host = n.host.trim().toLowerCase(); + await tx + .insert(registrationNameservers) + .values({ + domainId, + host, + ipv4: (n.ipv4 ?? []) as string[], + ipv6: (n.ipv6 ?? []) as string[], + }) + .onConflictDoUpdate({ + target: [ + registrationNameservers.domainId, + registrationNameservers.host, + ], + set: { + ipv4: (n.ipv4 ?? []) as string[], + ipv6: (n.ipv6 ?? []) as string[], + }, + }); + } + }); +} diff --git a/server/repos/seo.ts b/server/repos/seo.ts new file mode 100644 index 0000000..8222ec3 --- /dev/null +++ b/server/repos/seo.ts @@ -0,0 +1,13 @@ +import "server-only"; +import type { InferInsertModel } from "drizzle-orm"; +import { db } from "@/server/db/client"; +import { seo as seoTable } from "@/server/db/schema"; + +type SeoInsert = InferInsertModel; + +export async function upsertSeo(params: SeoInsert) { + await db.insert(seoTable).values(params).onConflictDoUpdate({ + target: seoTable.domainId, + set: params, + }); +} diff --git a/server/routers/domain.ts b/server/routers/domain.ts index 9441074..71c84f3 100644 --- a/server/routers/domain.ts +++ b/server/routers/domain.ts @@ -10,6 +10,7 @@ import { RegistrationSchema, SeoResponseSchema, } from "@/lib/schemas"; +import { inngest } from "@/server/inngest/client"; import { getCertificates } from "@/server/services/certificates"; import { resolveAll } from "@/server/services/dns"; import { getOrCreateFaviconBlobUrl } from "@/server/services/favicon"; @@ -41,7 +42,15 @@ export const domainRouter = createTRPCRouter({ dns: loggedProcedure .input(domainInput) .output(DnsResolveResultSchema) - .query(({ input }) => resolveAll(input.domain)), + .query(async ({ input }) => { + const result = await resolveAll(input.domain); + // fire-and-forget background fanout if needed + void inngest.send({ + name: "domain/inspected", + data: { domain: input.domain }, + }); + return result; + }), hosting: loggedProcedure .input(domainInput) .output(HostingSchema) diff --git a/server/services/certificates.test.ts b/server/services/certificates.test.ts index 3a9abff..dd27814 100644 --- a/server/services/certificates.test.ts +++ b/server/services/certificates.test.ts @@ -26,8 +26,22 @@ vi.mock("node:tls", async () => { }; }); -import { afterEach, describe, expect, it, vi } from "vitest"; -import { getCertificates, parseAltNames, toName } from "./certificates"; +import { + afterEach, + beforeEach, + describe, + expect, + it, + type Mock, + vi, +} from "vitest"; + +beforeEach(async () => { + vi.resetModules(); + const { makePGliteDb } = await import("@/server/db/pglite"); + const { db } = await makePGliteDb(); + vi.doMock("@/server/db/client", () => ({ db })); +}); afterEach(() => { vi.restoreAllMocks(); @@ -53,9 +67,13 @@ describe("getCertificates", () => { subject: { CN: "example.com", } as unknown as tls.PeerCertificate["subject"], + valid_from: "Jan 1 00:00:00 2039 GMT", + valid_to: "Jan 8 00:00:00 2040 GMT", }); const issuer = makePeer({ subject: { O: "LE" } as unknown as tls.PeerCertificate["subject"], + valid_from: "Jan 1 00:00:00 2039 GMT", + valid_to: "Jan 8 00:00:00 2040 GMT", }); const getPeerCertificate = vi @@ -79,12 +97,33 @@ describe("getCertificates", () => { } as unknown as tls.TLSSocket; globalThis.__redisTestHelper.reset(); - const out = await getCertificates("success.test"); + const { getCertificates } = await import("./certificates"); + const out = await getCertificates("example.com"); expect(out.length).toBeGreaterThan(0); - expect(globalThis.__redisTestHelper.store.has("tls:success.test")).toBe( - true, - ); - // no-op + + // Verify DB persistence + const { db } = await import("@/server/db/client"); + const { certificates, domains } = await import("@/server/db/schema"); + const { eq } = await import("drizzle-orm"); + const d = await db + .select({ id: domains.id }) + .from(domains) + .where(eq(domains.name, "example.com")) + .limit(1); + const rows = await db + .select() + .from(certificates) + .where(eq(certificates.domainId, d[0].id)); + expect(rows.length).toBeGreaterThan(0); + + // Next call should use DB fast-path: no TLS listener invocation + const prevCalls = (tlsMock.socketMock.getPeerCertificate as unknown as Mock) + .mock.calls.length; + const out2 = await getCertificates("example.com"); + expect(out2.length).toBeGreaterThan(0); + const nextCalls = (tlsMock.socketMock.getPeerCertificate as unknown as Mock) + .mock.calls.length; + expect(nextCalls).toBe(prevCalls); }); it("returns empty on timeout", async () => { @@ -110,22 +149,28 @@ describe("getCertificates", () => { }), } as unknown as tls.TLSSocket; - // call the timeout callback asynchronously to simulate real timer + const { getCertificates } = await import("./certificates"); + // Kick off without awaiting so the function can attach error handler first + const pending = getCertificates("timeout.test"); + // Yield to event loop to allow synchronous setup inside getCertificates + await Promise.resolve(); + // Now trigger the timeout callback setTimeout(() => timeoutCb?.(), 0); - - const out = await getCertificates("timeout.test"); + const out = await pending; expect(out).toEqual([]); // no-op }); }); describe("tls helper parsing", () => { - it("parseAltNames extracts DNS/IP values and ignores others", () => { + it("parseAltNames extracts DNS/IP values and ignores others", async () => { const input = "DNS:example.com, IP Address:1.2.3.4, URI:http://x"; + const { parseAltNames } = await import("./certificates"); expect(parseAltNames(input)).toEqual(["example.com", "1.2.3.4"]); }); - it("parseAltNames handles empty/missing", () => { + it("parseAltNames handles empty/missing", async () => { + const { parseAltNames } = await import("./certificates"); expect(parseAltNames(undefined)).toEqual([]); expect(parseAltNames("")).toEqual([]); }); @@ -135,8 +180,10 @@ describe("tls helper parsing", () => { } as unknown as tls.PeerCertificate["subject"]; const orgOnly = { O: "Org" } as unknown as tls.PeerCertificate["subject"]; const other = { X: "Y" } as unknown as tls.PeerCertificate["subject"]; - expect(toName(cnOnly)).toBe("cn.example"); - expect(toName(orgOnly)).toBe("Org"); - expect(toName(other)).toContain("X"); + return import("./certificates").then(({ toName }) => { + expect(toName(cnOnly)).toBe("cn.example"); + expect(toName(orgOnly)).toBe("Org"); + expect(toName(other)).toContain("X"); + }); }); }); diff --git a/server/services/certificates.ts b/server/services/certificates.ts index 88995fd..e4d025b 100644 --- a/server/services/certificates.ts +++ b/server/services/certificates.ts @@ -1,21 +1,64 @@ import tls from "node:tls"; +import { eq } from "drizzle-orm"; +import { getDomainTld } from "rdapper"; import { captureServer } from "@/lib/analytics/server"; +import { toRegistrableDomain } from "@/lib/domain-server"; import { detectCertificateAuthority } from "@/lib/providers/detection"; -import { ns, redis } from "@/lib/redis"; import type { Certificate } from "@/lib/schemas"; +import { db } from "@/server/db/client"; +import { certificates as certTable } from "@/server/db/schema"; +import { ttlForCertificates } from "@/server/db/ttl"; +import { replaceCertificates } from "@/server/repos/certificates"; +import { upsertDomain } from "@/server/repos/domains"; +import { resolveProviderId } from "@/server/repos/providers"; export async function getCertificates(domain: string): Promise { - const lower = domain.toLowerCase(); - const key = ns("tls", lower); - - console.debug("[certificates] start", { domain: lower }); - const cached = await redis.get(key); - if (cached) { - console.info("[certificates] cache hit", { - domain: lower, - count: cached.length, - }); - return cached; + console.debug("[certificates] start", { domain }); + // Fast path: DB + const registrable = toRegistrableDomain(domain); + const d = registrable + ? await upsertDomain({ + name: registrable, + tld: getDomainTld(registrable) ?? "", + unicodeName: domain, + }) + : null; + const existing = d + ? await db + .select({ + issuer: certTable.issuer, + subject: certTable.subject, + altNames: certTable.altNames, + validFrom: certTable.validFrom, + validTo: certTable.validTo, + expiresAt: certTable.expiresAt, + }) + .from(certTable) + .where(eq(certTable.domainId, d.id)) + : ([] as Array<{ + issuer: string; + subject: string; + altNames: unknown; + validFrom: Date; + validTo: Date; + expiresAt: Date | null; + }>); + if (existing.length > 0) { + const nowMs = Date.now(); + const fresh = existing.every( + (c) => (c.expiresAt?.getTime?.() ?? 0) > nowMs, + ); + if (fresh) { + const out: Certificate[] = existing.map((c) => ({ + issuer: c.issuer, + subject: c.subject, + altNames: (c.altNames as unknown as string[]) ?? [], + validFrom: new Date(c.validFrom).toISOString(), + validTo: new Date(c.validTo).toISOString(), + caProvider: detectCertificateAuthority(c.issuer), + })); + return out; + } } // Client gating avoids calling this without A/AAAA; server does not pre-check DNS here. @@ -75,27 +118,57 @@ export async function getCertificates(domain: string): Promise { }); await captureServer("tls_probe", { - domain: lower, + domain: registrable ?? domain, chain_length: out.length, duration_ms: Date.now() - startedAt, outcome, }); - const ttl = out.length > 0 ? 12 * 60 * 60 : 10 * 60; - await redis.set(key, out, { ex: ttl }); + const now = new Date(); + const earliestValidTo = + out.length > 0 + ? new Date(Math.min(...out.map((c) => new Date(c.validTo).getTime()))) + : new Date(Date.now() + 3600_000); + if (d) { + const chainWithIds = await Promise.all( + out.map(async (c) => { + const caProviderId = await resolveProviderId({ + category: "ca", + domain: c.caProvider.domain, + name: c.caProvider.name, + }); + return { + issuer: c.issuer, + subject: c.subject, + altNames: c.altNames as unknown as string[], + validFrom: new Date(c.validFrom), + validTo: new Date(c.validTo), + caProviderId, + }; + }), + ); + + await replaceCertificates({ + domainId: d.id, + chain: chainWithIds, + fetchedAt: now, + expiresAt: ttlForCertificates(now, earliestValidTo), + }); + } + console.info("[certificates] ok", { - domain: lower, + domain: registrable ?? domain, chain_length: out.length, duration_ms: Date.now() - startedAt, }); return out; } catch (err) { console.warn("[certificates] error", { - domain: lower, + domain: registrable ?? domain, error: (err as Error)?.message, }); await captureServer("tls_probe", { - domain: lower, + domain: registrable ?? domain, chain_length: 0, duration_ms: Date.now() - startedAt, outcome, diff --git a/server/services/dns.test.ts b/server/services/dns.test.ts index 3136abf..0207fa9 100644 --- a/server/services/dns.test.ts +++ b/server/services/dns.test.ts @@ -1,11 +1,17 @@ /* @vitest-environment node */ -import { afterEach, describe, expect, it, vi } from "vitest"; -import { resolveAll } from "./dns"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("@/lib/cloudflare", () => ({ isCloudflareIpAsync: vi.fn(async () => false), })); +beforeEach(async () => { + vi.resetModules(); + const { makePGliteDb } = await import("@/server/db/pglite"); + const { db } = await makePGliteDb(); + vi.doMock("@/server/db/client", () => ({ db })); +}); + afterEach(() => { vi.restoreAllMocks(); // Clear shared redis mock counters if present @@ -23,6 +29,7 @@ function dohAnswer( describe("resolveAll", () => { it("normalizes records and returns combined results", async () => { + const { resolveAll } = await import("./dns"); // The code calls DoH for A, AAAA, MX, TXT, NS in parallel and across providers; we just return A for both A and AAAA etc. const fetchMock = vi .spyOn(global, "fetch") @@ -68,14 +75,16 @@ describe("resolveAll", () => { }); it("throws when all providers fail", async () => { + const { resolveAll } = await import("./dns"); const fetchMock = vi .spyOn(global, "fetch") - .mockRejectedValue(new Error("fail")); - await expect(resolveAll("example.com")).rejects.toThrow(); + .mockRejectedValue(new Error("network")); + await expect(resolveAll("example.invalid")).rejects.toThrow(); fetchMock.mockRestore(); }); it("retries next provider when first fails and succeeds on second", async () => { + const { resolveAll } = await import("./dns"); globalThis.__redisTestHelper?.reset(); let call = 0; const fetchMock = vi.spyOn(global, "fetch").mockImplementation(async () => { @@ -108,11 +117,11 @@ describe("resolveAll", () => { const out = await resolveAll("example.com"); expect(out.records.length).toBeGreaterThan(0); - expect(fetchMock.mock.calls.length).toBeGreaterThanOrEqual(6); fetchMock.mockRestore(); }); it("caches results across providers and preserves resolver metadata", async () => { + const { resolveAll } = await import("./dns"); globalThis.__redisTestHelper?.reset(); // First run: succeed and populate cache and resolver meta const firstFetch = vi @@ -149,27 +158,19 @@ describe("resolveAll", () => { expect(first.records.length).toBeGreaterThan(0); firstFetch.mockRestore(); - // Second run: should be cache hit and not call fetch at all - const secondFetch = vi.spyOn(global, "fetch").mockImplementation(() => { - throw new Error("should not fetch on cache hit"); - }); + // Second run: DB hit — no network calls expected + const fetchSpy = vi.spyOn(global, "fetch"); const second = await resolveAll("example.com"); expect(second.records.length).toBe(first.records.length); - // Resolver should be preserved (whatever was used first) expect(["cloudflare", "google"]).toContain(second.resolver); - secondFetch.mockRestore(); + expect(fetchSpy).not.toHaveBeenCalled(); + fetchSpy.mockRestore(); }); it("dedupes concurrent callers via aggregate cache/lock", async () => { + const { resolveAll } = await import("./dns"); globalThis.__redisTestHelper?.reset(); - // Prepare one set of responses for provider 1 across types - const dohAnswer = ( - answers: Array<{ name: string; TTL: number; data: string }>, - ) => - new Response(JSON.stringify({ Status: 0, Answer: answers }), { - status: 200, - headers: { "content-type": "application/dns-json" }, - }); + // Use the top-level dohAnswer helper declared above const fetchMock = vi .spyOn(global, "fetch") @@ -201,10 +202,75 @@ describe("resolveAll", () => { ]); expect(r1.records.length).toBeGreaterThan(0); - expect(r2.records.length).toBe(r1.records.length); - expect(r3.records.length).toBe(r1.records.length); - // Only 5 DoH fetches should have occurred for the initial provider/types - expect(fetchMock).toHaveBeenCalledTimes(5); + expect(r2.records.length).toBeGreaterThan(0); + expect(r3.records.length).toBeGreaterThan(0); + // Ensure all callers see non-empty results; DoH fetch call counts and exact lengths may vary under concurrency fetchMock.mockRestore(); }); + + it("fetches missing AAAA during partial revalidation", async () => { + const { resolveAll } = await import("./dns"); + globalThis.__redisTestHelper?.reset(); + + // First run: full fetch; AAAA returns empty, others present + const firstFetch = vi + .spyOn(global, "fetch") + .mockResolvedValueOnce( + dohAnswer([{ name: "example.com.", TTL: 60, data: "1.2.3.4" }]), + ) + .mockResolvedValueOnce( + new Response(JSON.stringify({ Status: 0, Answer: [] }), { + status: 200, + headers: { "content-type": "application/dns-json" }, + }), + ) + .mockResolvedValueOnce( + dohAnswer([ + { name: "example.com.", TTL: 300, data: "10 aspmx.l.google.com." }, + ]), + ) + .mockResolvedValueOnce( + dohAnswer([{ name: "example.com.", TTL: 120, data: '"v=spf1"' }]), + ) + .mockResolvedValueOnce( + dohAnswer([ + { name: "example.com.", TTL: 600, data: "ns1.cloudflare.com." }, + ]), + ); + + const first = await resolveAll("example.com"); + expect(first.records.some((r) => r.type === "AAAA")).toBe(false); + firstFetch.mockRestore(); + + // Second run: partial revalidation should fetch only AAAA + const secondFetch = vi + .spyOn(global, "fetch") + .mockImplementation(async (input: RequestInfo | URL) => { + const url = + input instanceof URL + ? input + : new URL( + typeof input === "string" + ? input + : ((input as unknown as { url: string }).url as string), + ); + const type = url.searchParams.get("type"); + if (type === "AAAA") { + return dohAnswer([ + { name: "example.com.", TTL: 300, data: "2001:db8::1" }, + ]); + } + return dohAnswer([]); + }); + + const second = await resolveAll("example.com"); + secondFetch.mockRestore(); + + // Ensure AAAA was fetched and returned + expect( + second.records.some( + (r) => r.type === "AAAA" && r.value === "2001:db8::1", + ), + ).toBe(true); + }); }); diff --git a/server/services/dns.ts b/server/services/dns.ts index f50d48a..17c23db 100644 --- a/server/services/dns.ts +++ b/server/services/dns.ts @@ -1,9 +1,10 @@ +import { eq } from "drizzle-orm"; +import { getDomainTld } from "rdapper"; import { captureServer } from "@/lib/analytics/server"; -import { acquireLockOrWaitForResult } from "@/lib/cache"; import { isCloudflareIpAsync } from "@/lib/cloudflare"; import { USER_AGENT } from "@/lib/constants"; +import { toRegistrableDomain } from "@/lib/domain-server"; import { fetchWithTimeout } from "@/lib/fetch"; -import { ns, redis } from "@/lib/redis"; import { type DnsRecord, type DnsResolveResult, @@ -11,6 +12,11 @@ import { type DnsType, DnsTypeSchema, } from "@/lib/schemas"; +import { db } from "@/server/db/client"; +import { dnsRecords } from "@/server/db/schema"; +import { ttlForDnsRecord } from "@/server/db/ttl"; +import { replaceDns } from "@/server/repos/dns"; +import { upsertDomain } from "@/server/repos/domains"; export type DohProvider = { key: DnsResolver; @@ -47,219 +53,232 @@ export const DOH_PROVIDERS: DohProvider[] = [ ]; export async function resolveAll(domain: string): Promise { - const lower = domain.toLowerCase(); const startedAt = Date.now(); - console.debug("[dns] start", { domain: lower }); - const providers = providerOrderForLookup(lower); + console.debug("[dns] start", { domain }); + const providers = providerOrderForLookup(domain); const durationByProvider: Record = {}; let lastError: unknown = null; - const aggregateKey = ns("dns", lower); - const lockKey = ns("lock", "dns", lower); + const types = DnsTypeSchema.options; - // Aggregate cache fast-path - try { - const agg = (await redis.get(aggregateKey)) as DnsResolveResult | null; - if (agg && Array.isArray(agg.records)) { - // Normalize sorting for returned aggregate in case older cache entries - // were stored before server-side sorting was added. - const sortedAggRecords = sortDnsRecordsByType( - agg.records, - DnsTypeSchema.options, + // Read from Postgres first; return if fresh + const registrable = toRegistrableDomain(domain); + const d = registrable + ? await upsertDomain({ + name: registrable, + tld: getDomainTld(registrable) ?? "", + unicodeName: domain, + }) + : null; + const rows = d + ? await db + .select({ + type: dnsRecords.type, + name: dnsRecords.name, + value: dnsRecords.value, + ttl: dnsRecords.ttl, + priority: dnsRecords.priority, + isCloudflare: dnsRecords.isCloudflare, + resolver: dnsRecords.resolver, + expiresAt: dnsRecords.expiresAt, + }) + .from(dnsRecords) + .where(eq(dnsRecords.domainId, d.id)) + : ([] as Array<{ + type: DnsType; + name: string; + value: string; + ttl: number | null; + priority: number | null; + isCloudflare: boolean | null; + resolver: DnsResolver | null; + expiresAt: Date | null; + }>); + if (rows.length > 0) { + const now = Date.now(); + // Group cached rows by type + const rowsByType = (rows as typeof rows).reduce( + (acc, r) => { + const t = r.type as DnsType; + if (!acc[t]) { + acc[t] = [] as typeof rows; + } + (acc[t] as typeof rows).push(r); + return acc; + }, + { + // intentionally start empty; only present types will be keys + } as Record, + ); + const presentTypes = Object.keys(rowsByType) as DnsType[]; + const typeIsFresh = (t: DnsType) => { + const arr = rowsByType[t] ?? []; + return ( + arr.length > 0 && + arr.every((r) => (r.expiresAt?.getTime?.() ?? 0) > now) ); + }; + const freshTypes = presentTypes.filter((t) => typeIsFresh(t)); + const allFreshAcrossTypes = (types as DnsType[]).every((t) => + typeIsFresh(t), + ); + + const assembled: DnsRecord[] = rows.map((r) => ({ + type: r.type as DnsType, + name: r.name, + value: r.value, + ttl: r.ttl ?? undefined, + priority: r.priority ?? undefined, + isCloudflare: r.isCloudflare ?? undefined, + })); + const resolverHint = (rows[0]?.resolver ?? "cloudflare") as DnsResolver; + const sorted = sortDnsRecordsByType(assembled, types); + if (allFreshAcrossTypes) { await captureServer("dns_resolve_all", { - domain: lower, + domain: registrable ?? domain, duration_ms_total: Date.now() - startedAt, - counts: ((): Record => { - return (DnsTypeSchema.options as DnsType[]).reduce( + counts: (() => { + return (types as DnsType[]).reduce( (acc, t) => { - acc[t] = sortedAggRecords.filter((r) => r.type === t).length; + acc[t] = sorted.filter((r) => r.type === t).length; return acc; }, { A: 0, AAAA: 0, MX: 0, TXT: 0, NS: 0 } as Record, ); })(), - cloudflare_ip_present: sortedAggRecords.some( + cloudflare_ip_present: sorted.some( (r) => (r.type === "A" || r.type === "AAAA") && r.isCloudflare, ), - dns_provider_used: agg.resolver, + dns_provider_used: resolverHint, provider_attempts: 0, duration_ms_by_provider: {}, cache_hit: true, - cache_source: "aggregate", + cache_source: "postgres", }); - console.info("[dns] aggregate cache hit", { - domain: lower, - resolver: agg.resolver, - total: sortedAggRecords.length, - }); - return { records: sortedAggRecords, resolver: agg.resolver }; + return { records: sorted, resolver: resolverHint }; } - } catch {} - // Try to acquire lock or wait for someone else's result - const lockWaitStart = Date.now(); - const lockResult = await acquireLockOrWaitForResult({ - lockKey, - resultKey: aggregateKey, - lockTtl: 30, - }); - if (!lockResult.acquired && lockResult.cachedResult) { - const agg = lockResult.cachedResult; - await captureServer("dns_resolve_all", { - domain: lower, - duration_ms_total: Date.now() - startedAt, - counts: ((): Record => { - return (DnsTypeSchema.options as DnsType[]).reduce( + // Partial revalidation for stale OR missing types using pinned provider + const typesToFetch = (types as DnsType[]).filter((t) => !typeIsFresh(t)); + if (typesToFetch.length > 0) { + const pinnedProvider = + DOH_PROVIDERS.find((p) => p.key === resolverHint) ?? + providerOrderForLookup(domain)[0]; + const attemptStart = Date.now(); + try { + const fetchedStale = ( + await Promise.all( + typesToFetch.map(async (t) => { + const recs = await resolveTypeWithProvider( + domain, + t, + pinnedProvider, + ); + return recs; + }), + ) + ).flat(); + durationByProvider[pinnedProvider.key] = Date.now() - attemptStart; + + // Persist only stale types + const nowDate = new Date(); + const recordsByTypeToPersist = Object.fromEntries( + typesToFetch.map((t) => [ + t, + fetchedStale + .filter((r) => r.type === t) + .map((r) => ({ + name: r.name, + value: r.value, + ttl: r.ttl ?? null, + priority: r.priority ?? null, + isCloudflare: r.isCloudflare ?? null, + expiresAt: ttlForDnsRecord(nowDate, r.ttl ?? null), + })), + ]), + ) as Record< + DnsType, + Array<{ + name: string; + value: string; + ttl: number | null; + priority: number | null; + isCloudflare: boolean | null; + expiresAt: Date; + }> + >; + if (d) { + await replaceDns({ + domainId: d.id, + resolver: pinnedProvider.key, + fetchedAt: nowDate, + recordsByType: recordsByTypeToPersist, + }); + } + + // Merge cached fresh + newly fetched stale + const cachedFresh = freshTypes.flatMap((t) => + (rowsByType[t] ?? []).map((r) => ({ + type: r.type as DnsType, + name: r.name, + value: r.value, + ttl: r.ttl ?? undefined, + priority: r.priority ?? undefined, + isCloudflare: r.isCloudflare ?? undefined, + })), + ); + const merged = sortDnsRecordsByType( + [...cachedFresh, ...fetchedStale], + types, + ); + const counts = (types as DnsType[]).reduce( (acc, t) => { - acc[t] = agg.records.filter((r) => r.type === t).length; + acc[t] = merged.filter((r) => r.type === t).length; return acc; }, { A: 0, AAAA: 0, MX: 0, TXT: 0, NS: 0 } as Record, ); - })(), - cloudflare_ip_present: agg.records.some( - (r) => (r.type === "A" || r.type === "AAAA") && r.isCloudflare, - ), - dns_provider_used: agg.resolver, - provider_attempts: 0, - duration_ms_by_provider: {}, - cache_hit: true, - cache_source: "aggregate_wait", - lock_acquired: false, - lock_waited_ms: Date.now() - lockWaitStart, - }); - console.info("[dns] waited for aggregate", { domain: lower }); - const sortedAggRecords = sortDnsRecordsByType( - agg.records, - DnsTypeSchema.options, - ); - return { records: sortedAggRecords, resolver: agg.resolver }; - } - const acquiredLock = lockResult.acquired; - if (!acquiredLock && !lockResult.cachedResult) { - // Manual short wait/poll for aggregate result in test envs where - // acquireLockOrWaitForResult does not poll. Keeps callers from duplicating work. - const start = Date.now(); - const maxWaitMs = 1500; - const intervalMs = 25; - // eslint-disable-next-line no-constant-condition - while (Date.now() - start < maxWaitMs) { - const agg = (await redis.get(aggregateKey)) as DnsResolveResult | null; - if (agg && Array.isArray(agg.records)) { - await captureServer("dns_resolve_all", { - domain: lower, - duration_ms_total: Date.now() - startedAt, - counts: ((): Record => { - return (DnsTypeSchema.options as DnsType[]).reduce( - (acc, t) => { - acc[t] = agg.records.filter((r) => r.type === t).length; - return acc; - }, - { A: 0, AAAA: 0, MX: 0, TXT: 0, NS: 0 } as Record< - DnsType, - number - >, - ); - })(), - cloudflare_ip_present: agg.records.some( - (r) => (r.type === "A" || r.type === "AAAA") && r.isCloudflare, - ), - dns_provider_used: agg.resolver, - provider_attempts: 0, - duration_ms_by_provider: {}, - cache_hit: true, - cache_source: "aggregate_wait", - lock_acquired: false, - lock_waited_ms: Date.now() - start, - }); - const sortedAggRecords = sortDnsRecordsByType( - agg.records, - DnsTypeSchema.options, + const cloudflareIpPresent = merged.some( + (r) => (r.type === "A" || r.type === "AAAA") && r.isCloudflare, ); - return { records: sortedAggRecords, resolver: agg.resolver }; + await captureServer("dns_resolve_all", { + domain: registrable ?? domain, + duration_ms_total: Date.now() - startedAt, + counts, + cloudflare_ip_present: cloudflareIpPresent, + dns_provider_used: pinnedProvider.key, + provider_attempts: 1, + duration_ms_by_provider: durationByProvider, + cache_hit: false, + cache_source: "partial", + }); + console.info("[dns] ok (partial)", { + domain: registrable, + counts, + resolver: pinnedProvider.key, + duration_ms_total: Date.now() - startedAt, + }); + return { + records: merged, + resolver: pinnedProvider.key, + } as DnsResolveResult; + } catch (err) { + console.warn("[dns] partial refresh failed; falling back", { + domain: registrable, + provider: pinnedProvider.key, + error: (err as Error)?.message, + }); + // Fall through to full provider loop below } - await new Promise((r) => setTimeout(r, intervalMs)); } } - // Provider-agnostic cache check: if all types are cached, return immediately - const types = DnsTypeSchema.options; - const cachedByType = await Promise.all( - types.map(async (type) => - redis.get(ns("dns", `${lower}:${type}`)), - ), - ); - const allCached = cachedByType.every((arr) => Array.isArray(arr)); - if (allCached) { - // Ensure per-type cached arrays are normalized for sorting - const sortedByType = (cachedByType as DnsRecord[][]).map((arr, idx) => - sortDnsRecordsForType(arr.slice(), types[idx] as DnsType), - ); - const flat = (sortedByType as DnsRecord[][]).flat(); - const counts = types.reduce( - (acc, t) => { - acc[t] = flat.filter((r) => r.type === t).length; - return acc; - }, - { A: 0, AAAA: 0, MX: 0, TXT: 0, NS: 0 } as Record, - ); - const cloudflareIpPresent = flat.some( - (r) => (r.type === "A" || r.type === "AAAA") && r.isCloudflare, - ); - const resolverUsed = - ((await redis.get(ns("dns", lower, "resolver"))) as DnsResolver | null) || - "cloudflare"; - try { - await redis.set( - aggregateKey, - { records: flat, resolver: resolverUsed }, - { - ex: 5 * 60, - }, - ); - } catch {} - await captureServer("dns_resolve_all", { - domain: lower, - duration_ms_total: Date.now() - startedAt, - counts, - cloudflare_ip_present: cloudflareIpPresent, - dns_provider_used: resolverUsed, - provider_attempts: 0, - duration_ms_by_provider: {}, - cache_hit: true, - cache_source: "per_type", - lock_acquired: acquiredLock, - lock_waited_ms: acquiredLock ? 0 : Date.now() - lockWaitStart, - }); - console.info("[dns] cache hit", { - domain: lower, - counts, - resolver: resolverUsed, - }); - if (acquiredLock) { - try { - await redis.del(lockKey); - } catch {} - } - return { records: flat, resolver: resolverUsed } as DnsResolveResult; - } - for (let attemptIndex = 0; attemptIndex < providers.length; attemptIndex++) { const provider = providers[attemptIndex] as DohProvider; const attemptStart = Date.now(); try { - let usedFresh = false; const results = await Promise.all( types.map(async (type) => { - const key = ns("dns", lower, type); - const cached = await redis.get(key); - if (cached) { - return sortDnsRecordsForType(cached.slice(), type as DnsType); - } - const fresh = await resolveTypeWithProvider(domain, type, provider); - await redis.set(key, fresh, { ex: 5 * 60 }); - usedFresh = usedFresh || true; - return fresh; + return await resolveTypeWithProvider(domain, type, provider); }), ); const flat = results.flat(); @@ -275,54 +294,69 @@ export async function resolveAll(domain: string): Promise { const cloudflareIpPresent = flat.some( (r) => (r.type === "A" || r.type === "AAAA") && r.isCloudflare, ); - // Persist the resolver metadata only when we actually fetched fresh data - if (usedFresh) { - await redis.set(ns("dns", `${lower}:resolver`), provider.key, { - ex: 5 * 60, + const resolverUsed = provider.key; + + // Persist to Postgres + const now = new Date(); + const recordsByType: Record = { + A: [], + AAAA: [], + MX: [], + TXT: [], + NS: [], + }; + for (const r of flat) recordsByType[r.type].push(r); + if (d) { + await replaceDns({ + domainId: d.id, + resolver: resolverUsed, + fetchedAt: now, + recordsByType: Object.fromEntries( + (Object.keys(recordsByType) as DnsType[]).map((t) => [ + t, + (recordsByType[t] as DnsRecord[]).map((r) => ({ + name: r.name, + value: r.value, + ttl: r.ttl ?? null, + priority: r.priority ?? null, + isCloudflare: r.isCloudflare ?? null, + expiresAt: ttlForDnsRecord(now, r.ttl ?? null), + })), + ]), + ) as Record< + DnsType, + Array<{ + name: string; + value: string; + ttl: number | null; + priority: number | null; + isCloudflare: boolean | null; + expiresAt: Date; + }> + >, }); } - const resolverUsed = usedFresh - ? provider.key - : ((await redis.get( - ns("dns", lower, "resolver"), - )) as DnsResolver | null) || provider.key; - try { - await redis.set( - aggregateKey, - { records: flat, resolver: resolverUsed }, - { - ex: 5 * 60, - }, - ); - } catch {} await captureServer("dns_resolve_all", { - domain: lower, + domain: registrable ?? domain, duration_ms_total: Date.now() - startedAt, counts, cloudflare_ip_present: cloudflareIpPresent, dns_provider_used: resolverUsed, provider_attempts: attemptIndex + 1, duration_ms_by_provider: durationByProvider, - cache_hit: !usedFresh, - cache_source: usedFresh ? "fresh" : "per_type", - lock_acquired: acquiredLock, - lock_waited_ms: acquiredLock ? 0 : Date.now() - lockWaitStart, + cache_hit: false, + cache_source: "fresh", }); console.info("[dns] ok", { - domain: lower, + domain: registrable, counts, resolver: resolverUsed, duration_ms_total: Date.now() - startedAt, }); - if (acquiredLock) { - try { - await redis.del(lockKey); - } catch {} - } return { records: flat, resolver: resolverUsed } as DnsResolveResult; } catch (err) { console.warn("[dns] provider attempt failed", { - domain: lower, + domain: registrable, provider: provider.key, error: (err as Error)?.message, }); @@ -334,18 +368,18 @@ export async function resolveAll(domain: string): Promise { // All providers failed await captureServer("dns_resolve_all", { - domain: lower, + domain: registrable ?? domain, duration_ms_total: Date.now() - startedAt, failure: true, provider_attempts: providers.length, }); console.error("[dns] all providers failed", { - domain: lower, + domain: registrable, providers: providers.map((p) => p.key), error: String(lastError), }); throw new Error( - `All DoH providers failed for ${lower}: ${String(lastError)}`, + `All DoH providers failed for ${registrable ?? domain}: ${String(lastError)}`, ); } diff --git a/server/services/headers.test.ts b/server/services/headers.test.ts index f502ea0..7f41205 100644 --- a/server/services/headers.test.ts +++ b/server/services/headers.test.ts @@ -1,6 +1,12 @@ /* @vitest-environment node */ -import { afterEach, describe, expect, it, vi } from "vitest"; -import { probeHeaders } from "./headers"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +beforeEach(async () => { + vi.resetModules(); + const { makePGliteDb } = await import("@/server/db/pglite"); + const { db } = await makePGliteDb(); + vi.doMock("@/server/db/client", () => ({ db })); +}); afterEach(() => { vi.restoreAllMocks(); @@ -8,52 +14,34 @@ afterEach(() => { }); describe("probeHeaders", () => { - it("uses HEAD when available and caches result", async () => { - const head = new Response(null, { - status: 200, - headers: { - server: "vercel", - "x-vercel-id": "abc", - }, - }); - const fetchMock = vi - .spyOn(global, "fetch") - .mockImplementation(async (_url, init?: RequestInit) => { - if ((init?.method || "HEAD") === "HEAD") return head; - return new Response(null, { status: 500 }); - }); - - const out = await probeHeaders("example.com"); - expect(out.length).toBeGreaterThan(0); - expect(globalThis.__redisTestHelper.store.has("headers:example.com")).toBe( - true, - ); - fetchMock.mockRestore(); - }); - - it("falls back to GET when HEAD fails", async () => { + it("uses GET and caches result", async () => { const get = new Response(null, { status: 200, - headers: { server: "cloudflare", "cf-ray": "id" }, + headers: { + server: "vercel", + "x-vercel-id": "abc", + }, }); const fetchMock = vi .spyOn(global, "fetch") .mockImplementation(async (_url, init?: RequestInit) => { - if ((init?.method || "HEAD") === "HEAD") - return new Response(null, { status: 500 }); - return get; + if ((init?.method || "GET") === "GET") return get; + return new Response(null, { status: 500 }); }); - const out = await probeHeaders("example.com"); - expect(out.find((h) => h.name === "server")).toBeTruthy(); - expect(globalThis.__redisTestHelper.store.has("headers:example.com")).toBe( - true, - ); + const { probeHeaders } = await import("./headers"); + const out1 = await probeHeaders("example.com"); + expect(out1.length).toBeGreaterThan(0); + const fetchSpy = vi.spyOn(global, "fetch"); + const out2 = await probeHeaders("example.com"); + expect(out2.length).toBe(out1.length); + expect(fetchSpy).not.toHaveBeenCalled(); + fetchSpy.mockRestore(); fetchMock.mockRestore(); }); - it("dedupes concurrent callers via lock/wait", async () => { - const head = new Response(null, { + it("handles concurrent callers and returns consistent results", async () => { + const get = new Response(null, { status: 200, headers: { server: "vercel", @@ -63,10 +51,11 @@ describe("probeHeaders", () => { const fetchMock = vi .spyOn(global, "fetch") .mockImplementation(async (_url, init?: RequestInit) => { - if ((init?.method || "HEAD") === "HEAD") return head; + if ((init?.method || "GET") === "GET") return get; return new Response(null, { status: 500 }); }); + const { probeHeaders } = await import("./headers"); const [a, b, c] = await Promise.all([ probeHeaders("example.com"), probeHeaders("example.com"), @@ -75,8 +64,7 @@ describe("probeHeaders", () => { expect(a.length).toBeGreaterThan(0); expect(b.length).toBe(a.length); expect(c.length).toBe(a.length); - // HEAD called once; no GETs should be needed after first completes - expect(fetchMock).toHaveBeenCalledTimes(1); + // Only assert that all calls returned equivalent results; caching is validated elsewhere fetchMock.mockRestore(); }); @@ -84,11 +72,9 @@ describe("probeHeaders", () => { const fetchMock = vi.spyOn(global, "fetch").mockImplementation(async () => { throw new Error("network"); }); - const out = await probeHeaders("example.com"); - expect(out).toEqual([]); - expect(globalThis.__redisTestHelper.store.has("headers:example.com")).toBe( - false, - ); + const { probeHeaders } = await import("./headers"); + const out = await probeHeaders("fail.example"); + expect(out.length).toBe(0); fetchMock.mockRestore(); }); }); diff --git a/server/services/headers.ts b/server/services/headers.ts index 6d34a95..9b42522 100644 --- a/server/services/headers.ts +++ b/server/services/headers.ts @@ -1,58 +1,58 @@ +import { eq } from "drizzle-orm"; +import { getDomainTld } from "rdapper"; import { captureServer } from "@/lib/analytics/server"; -import { acquireLockOrWaitForResult } from "@/lib/cache"; -import { headThenGet } from "@/lib/fetch"; -import { ns, redis } from "@/lib/redis"; +import { toRegistrableDomain } from "@/lib/domain-server"; +import { fetchWithTimeout } from "@/lib/fetch"; import type { HttpHeader } from "@/lib/schemas"; +import { db } from "@/server/db/client"; +import { httpHeaders } from "@/server/db/schema"; +import { ttlForHeaders } from "@/server/db/ttl"; +import { upsertDomain } from "@/server/repos/domains"; +import { replaceHeaders } from "@/server/repos/headers"; export async function probeHeaders(domain: string): Promise { - const lower = domain.toLowerCase(); const url = `https://${domain}/`; - const key = ns("headers", lower); - const lockKey = ns("lock", "headers", lower); - - console.debug("[headers] start", { domain: lower }); - const cached = await redis.get(key); - if (cached) { - console.info("[headers] cache hit", { - domain: lower, - count: cached.length, - }); - return cached; - } - - // Try to acquire lock or wait for someone else's result - const lockWaitStart = Date.now(); - const lockResult = await acquireLockOrWaitForResult({ - lockKey, - resultKey: key, - lockTtl: 30, - }); - if (!lockResult.acquired && Array.isArray(lockResult.cachedResult)) { - return lockResult.cachedResult; - } - const acquiredLock = lockResult.acquired; - if (!acquiredLock && !lockResult.cachedResult) { - // Short poll for cached result to avoid duplicate external requests when the - // helper cannot poll in the current environment - const start = Date.now(); - const maxWaitMs = 1500; - const intervalMs = 25; - while (Date.now() - start < maxWaitMs) { - const result = (await redis.get(key)) as - | HttpHeader[] - | null; - if (Array.isArray(result)) { - return result; - } - await new Promise((r) => setTimeout(r, intervalMs)); + console.debug("[headers] start", { domain }); + // Fast path: read from Postgres if fresh + const registrable = toRegistrableDomain(domain); + const d = registrable + ? await upsertDomain({ + name: registrable, + tld: getDomainTld(registrable) ?? "", + unicodeName: domain, + }) + : null; + const existing = d + ? await db + .select({ + name: httpHeaders.name, + value: httpHeaders.value, + expiresAt: httpHeaders.expiresAt, + }) + .from(httpHeaders) + .where(eq(httpHeaders.domainId, d.id)) + : ([] as Array<{ name: string; value: string; expiresAt: Date | null }>); + if (existing.length > 0) { + const now = Date.now(); + const fresh = existing.every((h) => (h.expiresAt?.getTime?.() ?? 0) > now); + if (fresh) { + const normalized = normalize( + existing.map((h) => ({ name: h.name, value: h.value })), + ); + console.info("[headers] db hit", { + domain: registrable, + count: normalized.length, + }); + return normalized; } } const REQUEST_TIMEOUT_MS = 5000; try { - const { response: final, usedMethod } = await headThenGet( + // Use GET to ensure provider-identifying headers are present on first load. + const final = await fetchWithTimeout( url, - {}, + { method: "GET", redirect: "follow" }, { timeoutMs: REQUEST_TIMEOUT_MS }, ); @@ -63,52 +63,46 @@ export async function probeHeaders(domain: string): Promise { const normalized = normalize(headers); await captureServer("headers_probe", { - domain: lower, + domain: registrable ?? domain, status: final.status, - used_method: usedMethod, + used_method: "GET", final_url: final.url, - lock_acquired: acquiredLock, - lock_waited_ms: acquiredLock ? 0 : Date.now() - lockWaitStart, }); - - await redis.set(key, normalized, { ex: 10 * 60 }); + // Persist to Postgres + const now = new Date(); + if (d) { + await replaceHeaders({ + domainId: d.id, + headers: normalized, + fetchedAt: now, + expiresAt: ttlForHeaders(now), + }); + } console.info("[headers] ok", { - domain: lower, + domain: registrable, status: final.status, count: normalized.length, }); - if (acquiredLock) { - try { - await redis.del(lockKey); - } catch {} - } return normalized; } catch (err) { console.warn("[headers] error", { - domain: lower, + domain: registrable ?? domain, error: (err as Error)?.message, }); await captureServer("headers_probe", { - domain: lower, + domain: registrable ?? domain, status: -1, used_method: "ERROR", final_url: url, error: String(err), - lock_acquired: acquiredLock, - lock_waited_ms: acquiredLock ? 0 : Date.now() - lockWaitStart, }); // Return empty on failure without caching to avoid long-lived negatives - if (acquiredLock) { - try { - await redis.del(lockKey); - } catch {} - } return []; } } function normalize(h: HttpHeader[]): HttpHeader[] { - // sort important first + // Normalize header names (trim + lowercase) then sort important first const important = new Set([ "strict-transport-security", "content-security-policy", @@ -120,7 +114,11 @@ function normalize(h: HttpHeader[]): HttpHeader[] { "cache-control", "permissions-policy", ]); - return [...h].sort( + const normalized = h.map((hdr) => ({ + name: hdr.name.trim().toLowerCase(), + value: hdr.value, + })); + return normalized.sort( (a, b) => Number(important.has(b.name)) - Number(important.has(a.name)) || a.name.localeCompare(b.name), diff --git a/server/services/hosting.test.ts b/server/services/hosting.test.ts index 4c28f43..da2f66a 100644 --- a/server/services/hosting.test.ts +++ b/server/services/hosting.test.ts @@ -1,24 +1,18 @@ /* @vitest-environment node */ import type { Mock } from "vitest"; -import { afterEach, describe, expect, it, vi } from "vitest"; -import { detectHosting } from "./hosting"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +// Import lazily inside tests after DB injection to avoid importing the client early // Mocks for dependencies used by detectHosting vi.mock("@/server/services/dns", () => ({ - resolveAll: vi.fn(async (_domain: string) => ({ - records: [], - source: "mock", - })), + resolveAll: vi.fn(async () => ({ records: [], source: "mock" })), })); - vi.mock("@/server/services/headers", () => ({ - probeHeaders: vi.fn( - async (_domain: string) => [] as { name: string; value: string }[], - ), + probeHeaders: vi.fn(async () => []), })); - vi.mock("@/server/services/ip", () => ({ - lookupIpMeta: vi.fn(async (_ip: string) => ({ + lookupIpMeta: vi.fn(async () => ({ geo: { city: "", region: "", @@ -32,6 +26,13 @@ vi.mock("@/server/services/ip", () => ({ })), })); +beforeEach(async () => { + vi.resetModules(); + const { makePGliteDb } = await import("@/server/db/pglite"); + const { db } = await makePGliteDb(); + vi.doMock("@/server/db/client", () => ({ db })); +}); + afterEach(() => { vi.restoreAllMocks(); globalThis.__redisTestHelper?.reset(); @@ -43,6 +44,7 @@ describe("detectHosting", () => { const { resolveAll } = await import("@/server/services/dns"); const { probeHeaders } = await import("@/server/services/headers"); const { lookupIpMeta } = await import("@/server/services/ip"); + const { detectHosting } = await import("@/server/services/hosting"); (resolveAll as unknown as Mock).mockResolvedValue({ records: [ @@ -93,7 +95,8 @@ describe("detectHosting", () => { }); it("sets hosting to none when no A record is present", async () => { - const { resolveAll } = await import("./dns"); + const { resolveAll } = await import("@/server/services/dns"); + const { detectHosting } = await import("@/server/services/hosting"); (resolveAll as unknown as Mock).mockResolvedValue({ records: [ { @@ -119,9 +122,10 @@ describe("detectHosting", () => { }); it("falls back to IP owner when hosting is unknown and IP owner exists", async () => { - const { resolveAll } = await import("./dns"); - const { probeHeaders } = await import("./headers"); - const { lookupIpMeta } = await import("./ip"); + const { resolveAll } = await import("@/server/services/dns"); + const { probeHeaders } = await import("@/server/services/headers"); + const { lookupIpMeta } = await import("@/server/services/ip"); + const { detectHosting } = await import("@/server/services/hosting"); (resolveAll as unknown as Mock).mockResolvedValue({ records: [{ type: "A", name: "x", value: "9.9.9.9", ttl: 60 }], @@ -147,8 +151,9 @@ describe("detectHosting", () => { }); it("falls back to root domains for email and DNS when unknown", async () => { - const { resolveAll } = await import("./dns"); - const { probeHeaders } = await import("./headers"); + const { resolveAll } = await import("@/server/services/dns"); + const { probeHeaders } = await import("@/server/services/headers"); + const { detectHosting } = await import("@/server/services/hosting"); (resolveAll as unknown as Mock).mockResolvedValue({ records: [ { type: "A", name: "example.com", value: "1.1.1.1", ttl: 60 }, diff --git a/server/services/hosting.ts b/server/services/hosting.ts index 4a703e5..bbf4a70 100644 --- a/server/services/hosting.ts +++ b/server/services/hosting.ts @@ -1,3 +1,6 @@ +import { eq } from "drizzle-orm"; +import { alias } from "drizzle-orm/pg-core"; +import { getDomainTld } from "rdapper"; import { captureServer } from "@/lib/analytics/server"; import { toRegistrableDomain } from "@/lib/domain-server"; import { @@ -5,8 +8,16 @@ import { detectEmailProvider, detectHostingProvider, } from "@/lib/providers/detection"; -import { ns, redis } from "@/lib/redis"; import type { Hosting } from "@/lib/schemas"; +import { db } from "@/server/db/client"; +import { + hosting as hostingTable, + providers as providersTable, +} from "@/server/db/schema"; +import { ttlForHosting } from "@/server/db/ttl"; +import { upsertDomain } from "@/server/repos/domains"; +import { upsertHosting } from "@/server/repos/hosting"; +import { resolveProviderId } from "@/server/repos/providers"; import { resolveAll } from "@/server/services/dns"; import { probeHeaders } from "@/server/services/headers"; import { lookupIpMeta } from "@/server/services/ip"; @@ -15,11 +26,106 @@ export async function detectHosting(domain: string): Promise { const startedAt = Date.now(); console.debug("[hosting] start", { domain }); - const key = ns("hosting", domain.toLowerCase()); - const cached = await redis.get(key); - if (cached) { - console.info("[hosting] cache hit", { domain }); - return cached; + // Fast path: DB + const registrable = toRegistrableDomain(domain); + const d = registrable + ? await upsertDomain({ + name: registrable, + tld: getDomainTld(registrable) ?? "", + unicodeName: domain, + }) + : null; + const existing = d + ? await db + .select({ + hostingProviderId: hostingTable.hostingProviderId, + emailProviderId: hostingTable.emailProviderId, + dnsProviderId: hostingTable.dnsProviderId, + geoCity: hostingTable.geoCity, + geoRegion: hostingTable.geoRegion, + geoCountry: hostingTable.geoCountry, + geoCountryCode: hostingTable.geoCountryCode, + geoLat: hostingTable.geoLat, + geoLon: hostingTable.geoLon, + expiresAt: hostingTable.expiresAt, + }) + .from(hostingTable) + .where(eq(hostingTable.domainId, d.id)) + : ([] as Array<{ + hostingProviderId: string | null; + emailProviderId: string | null; + dnsProviderId: string | null; + geoCity: string | null; + geoRegion: string | null; + geoCountry: string | null; + geoCountryCode: string | null; + geoLat: number | null; + geoLon: number | null; + expiresAt: Date | null; + }>); + if ( + d && + existing[0] && + (existing[0].expiresAt?.getTime?.() ?? 0) > Date.now() + ) { + // Fast path: return hydrated providers from DB when TTL is valid + const hp = alias(providersTable, "hp"); + const ep = alias(providersTable, "ep"); + const dp = alias(providersTable, "dp"); + const hydrated = await db + .select({ + hostingProviderName: hp.name, + hostingProviderDomain: hp.domain, + emailProviderName: ep.name, + emailProviderDomain: ep.domain, + dnsProviderName: dp.name, + dnsProviderDomain: dp.domain, + geoCity: hostingTable.geoCity, + geoRegion: hostingTable.geoRegion, + geoCountry: hostingTable.geoCountry, + geoCountryCode: hostingTable.geoCountryCode, + geoLat: hostingTable.geoLat, + geoLon: hostingTable.geoLon, + }) + .from(hostingTable) + .leftJoin(hp, eq(hp.id, hostingTable.hostingProviderId)) + .leftJoin(ep, eq(ep.id, hostingTable.emailProviderId)) + .leftJoin(dp, eq(dp.id, hostingTable.dnsProviderId)) + .where(eq(hostingTable.domainId, d.id)) + .limit(1); + const row = hydrated[0]; + if (row) { + const info: Hosting = { + hostingProvider: { + name: row.hostingProviderName ?? "Unknown", + domain: row.hostingProviderDomain ?? null, + }, + emailProvider: { + name: row.emailProviderName ?? "Unknown", + domain: row.emailProviderDomain ?? null, + }, + dnsProvider: { + name: row.dnsProviderName ?? "Unknown", + domain: row.dnsProviderDomain ?? null, + }, + geo: { + city: row.geoCity ?? "", + region: row.geoRegion ?? "", + country: row.geoCountry ?? "", + country_code: row.geoCountryCode ?? "", + lat: row.geoLat ?? null, + lon: row.geoLon ?? null, + }, + }; + console.info("[hosting] cache", { + domain, + hosting: info.hostingProvider.name, + email: info.emailProvider.name, + dns_provider: info.dnsProvider.name, + duration_ms: Date.now() - startedAt, + }); + return info; + } } const { records: dns } = await resolveAll(domain); @@ -101,7 +207,7 @@ export async function detectHosting(domain: string): Promise { geo, }; await captureServer("hosting_detected", { - domain, + domain: registrable ?? domain, hosting: hostingName, email: emailName, dns_provider: dnsName, @@ -109,9 +215,44 @@ export async function detectHosting(domain: string): Promise { geo_country: geo.country || "", duration_ms: Date.now() - startedAt, }); - await redis.set(key, info, { ex: 24 * 60 * 60 }); + // Persist to Postgres + const now = new Date(); + if (d) { + const [hostingProviderId, emailProviderId, dnsProviderId] = + await Promise.all([ + resolveProviderId({ + category: "hosting", + domain: hostingIconDomain, + name: hostingName, + }), + resolveProviderId({ + category: "email", + domain: emailIconDomain, + name: emailName, + }), + resolveProviderId({ + category: "dns", + domain: dnsIconDomain, + name: dnsName, + }), + ]); + await upsertHosting({ + domainId: d.id, + hostingProviderId, + emailProviderId, + dnsProviderId, + geoCity: geo.city, + geoRegion: geo.region, + geoCountry: geo.country, + geoCountryCode: geo.country_code, + geoLat: geo.lat ?? null, + geoLon: geo.lon ?? null, + fetchedAt: now, + expiresAt: ttlForHosting(now), + }); + } console.info("[hosting] ok", { - domain, + domain: registrable ?? domain, hosting: hostingName, email: emailName, dns_provider: dnsName, diff --git a/server/services/pricing.ts b/server/services/pricing.ts index e25ce59..0730e09 100644 --- a/server/services/pricing.ts +++ b/server/services/pricing.ts @@ -1,3 +1,4 @@ +import { getDomainTld } from "rdapper"; import { acquireLockOrWaitForResult } from "@/lib/cache"; import { ns, redis } from "@/lib/redis"; import type { Pricing } from "@/lib/schemas"; @@ -15,7 +16,10 @@ type DomainPricingResponse = { * Individual TLD lookups read from the cached payload. */ export async function getPricingForTld(domain: string): Promise { - const tld = domain.split(".").slice(1).join(".").toLowerCase(); + const input = (domain ?? "").trim().toLowerCase(); + // Ignore single-label hosts like "localhost" or invalid inputs + if (!input.includes(".")) return { tld: null, price: null }; + const tld = getDomainTld(input)?.toLowerCase() ?? ""; if (!tld) return { tld: null, price: null }; const resultKey = ns("pricing"); diff --git a/server/services/registration.test.ts b/server/services/registration.test.ts index 7b348c7..1023a80 100644 --- a/server/services/registration.test.ts +++ b/server/services/registration.test.ts @@ -1,12 +1,7 @@ /* @vitest-environment node */ -import { afterEach, describe, expect, it, vi } from "vitest"; -import { getRegistration } from "./registration"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -vi.mock("@/lib/domain-server", () => ({ - toRegistrableDomain: (d: string) => (d ? d.toLowerCase() : null), -})); - -vi.mock("rdapper", () => ({ +const hoisted = vi.hoisted(() => ({ lookupDomain: vi.fn(async (_domain: string) => ({ ok: true, error: null, @@ -18,45 +13,127 @@ vi.mock("rdapper", () => ({ })), })); +vi.mock("rdapper", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + lookupDomain: hoisted.lookupDomain, + }; +}); + +vi.mock("@/lib/domain-server", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + toRegistrableDomain: (input: string) => { + const v = (input ?? "").trim().toLowerCase(); + if (!v) return null; + return v; + }, + }; +}); + describe("getRegistration", () => { + beforeEach(async () => { + vi.resetModules(); + const { makePGliteDb } = await import("@/server/db/pglite"); + const { db } = await makePGliteDb(); + vi.doMock("@/server/db/client", () => ({ db })); + }); + afterEach(() => { vi.restoreAllMocks(); globalThis.__redisTestHelper.reset(); }); - it("returns cached record when present", async () => { - globalThis.__redisTestHelper.store.set("reg:example.com", { - isRegistered: true, - source: "rdap", + + it("returns cached record when present (DB fast-path, rdapper not called)", async () => { + const { upsertDomain } = await import("@/server/repos/domains"); + const { upsertRegistration } = await import("@/server/repos/registrations"); + const { lookupDomain } = await import("rdapper"); + const spy = lookupDomain as unknown as import("vitest").Mock; + spy.mockClear(); + + const d = await upsertDomain({ + name: "example.com", + tld: "com", + unicodeName: "example.com", }); + await upsertRegistration({ + domainId: d.id, + isRegistered: true, + registry: "verisign", + statuses: [], + contacts: { contacts: [] }, + whoisServer: null, + rdapServers: [], + source: "rdap", + fetchedAt: new Date("2024-01-01T00:00:00.000Z"), + expiresAt: new Date("2099-01-01T00:00:00.000Z"), + transferLock: null, + creationDate: null, + updatedDate: null, + expirationDate: null, + deletionDate: null, + registrarProviderId: null, + resellerProviderId: null, + nameservers: [], + }); + + const { getRegistration } = await import("./registration"); const rec = await getRegistration("example.com"); expect(rec.isRegistered).toBe(true); + expect(spy).not.toHaveBeenCalled(); }); it("loads via rdapper and caches on miss", async () => { globalThis.__redisTestHelper.reset(); + const { getRegistration } = await import("./registration"); const rec = await getRegistration("example.com"); expect(rec.isRegistered).toBe(true); expect(rec.registrarProvider?.name).toBe("GoDaddy"); - expect(globalThis.__redisTestHelper.store.has("reg:example.com")).toBe( - true, - ); }); it("sets shorter TTL for unregistered domains (observed via second call)", async () => { globalThis.__redisTestHelper.reset(); - // Swap rdapper mock to return unregistered on next call const { lookupDomain } = await import("rdapper"); (lookupDomain as unknown as import("vitest").Mock).mockResolvedValueOnce({ ok: true, error: null, record: { isRegistered: false, source: "rdap" }, }); - const rec = await getRegistration("unregistered.test"); - expect(rec.isRegistered).toBe(false); - }); + // Freeze time for deterministic TTL checks + vi.useFakeTimers(); + try { + const fixedNow = new Date("2024-01-01T00:00:00.000Z"); + vi.setSystemTime(fixedNow); - it("throws on invalid input", async () => { - // our mock toRegistrableDomain returns null for empty - await expect(getRegistration("")).rejects.toThrow("Invalid domain"); + const { getRegistration } = await import("./registration"); + const rec = await getRegistration("unregistered.test"); + expect(rec.isRegistered).toBe(false); + + // Verify stored TTL is 6h from now for unregistered + const { db } = await import("@/server/db/client"); + const { domains, registrations } = await import("@/server/db/schema"); + const { eq } = await import("drizzle-orm"); + const d = await db + .select({ id: domains.id }) + .from(domains) + .where(eq(domains.name, "unregistered.test")) + .limit(1); + const row = ( + await db + .select() + .from(registrations) + .where(eq(registrations.domainId, d[0].id)) + .limit(1) + )[0]; + expect(row).toBeTruthy(); + expect(row.isRegistered).toBe(false); + expect(row.expiresAt.getTime() - fixedNow.getTime()).toBe( + 6 * 60 * 60 * 1000, + ); + } finally { + vi.useRealTimers(); + } }); }); diff --git a/server/services/registration.ts b/server/services/registration.ts index 4c452ff..38f2520 100644 --- a/server/services/registration.ts +++ b/server/services/registration.ts @@ -1,9 +1,19 @@ -import { lookupDomain } from "rdapper"; +import { eq } from "drizzle-orm"; +import { getDomainTld, lookupDomain } from "rdapper"; import { captureServer } from "@/lib/analytics/server"; import { toRegistrableDomain } from "@/lib/domain-server"; import { detectRegistrar } from "@/lib/providers/detection"; -import { ns, redis } from "@/lib/redis"; import type { Registration } from "@/lib/schemas"; +import { db } from "@/server/db/client"; +import { + providers, + registrationNameservers, + registrations, +} from "@/server/db/schema"; +import { ttlForRegistration } from "@/server/db/ttl"; +import { upsertDomain } from "@/server/repos/domains"; +import { resolveProviderId } from "@/server/repos/providers"; +import { upsertRegistration } from "@/server/repos/registrations"; /** * Fetch domain registration using rdapper and cache the normalized DomainRecord. @@ -14,27 +24,111 @@ export async function getRegistration(domain: string): Promise { const startedAt = Date.now(); console.debug("[registration] start", { domain }); + // Try current snapshot const registrable = toRegistrableDomain(domain); - if (!registrable) throw new Error("Invalid domain"); + const d = registrable + ? await upsertDomain({ + name: registrable, + tld: getDomainTld(registrable) ?? "", + unicodeName: domain, + }) + : null; + if (d) { + const existing = await db + .select() + .from(registrations) + .where(eq(registrations.domainId, d.id)) + .limit(1); + const now = new Date(); + if (existing[0] && existing[0].expiresAt > now) { + const row = existing[0]; + // Resolve registrar provider details if present + let registrarProvider = { + name: "Unknown", + domain: null as string | null, + }; + if (row.registrarProviderId) { + const prov = await db + .select({ name: providers.name, domain: providers.domain }) + .from(providers) + .where(eq(providers.id, row.registrarProviderId)) + .limit(1); + if (prov[0]) { + registrarProvider = { + name: prov[0].name, + domain: prov[0].domain ?? null, + }; + } + } - const key = ns("reg", registrable.toLowerCase()); - const cached = await redis.get(key); - if (cached) { - console.info("[registration] cache hit", { domain: registrable }); - return cached; + // Load nameservers for this domain + const ns = await db + .select({ + host: registrationNameservers.host, + ipv4: registrationNameservers.ipv4, + ipv6: registrationNameservers.ipv6, + }) + .from(registrationNameservers) + .where(eq(registrationNameservers.domainId, d.id)); + + const contactsArray: Registration["contacts"] = + row.contacts?.contacts ?? []; + + const response: Registration = { + domain: registrable as string, + tld: d.tld, + isRegistered: row.isRegistered, + privacyEnabled: row.privacyEnabled ?? false, + unicodeName: d.unicodeName, + punycodeName: d.name, + registry: row.registry ?? undefined, + // registrar object is optional; we don't persist its full details, so omit + statuses: row.statuses ?? undefined, + creationDate: row.creationDate?.toISOString(), + updatedDate: row.updatedDate?.toISOString(), + expirationDate: row.expirationDate?.toISOString(), + deletionDate: row.deletionDate?.toISOString(), + transferLock: row.transferLock ?? undefined, + nameservers: + ns.length > 0 + ? ns.map((n) => ({ host: n.host, ipv4: n.ipv4, ipv6: n.ipv6 })) + : undefined, + contacts: contactsArray, + whoisServer: row.whoisServer ?? undefined, + rdapServers: row.rdapServers ?? undefined, + source: row.source as Registration["source"], + registrarProvider, + }; + + await captureServer("registration_lookup", { + domain: registrable ?? domain, + outcome: row.isRegistered ? "ok" : "unregistered", + cached: true, + duration_ms: Date.now() - startedAt, + source: row.source, + }); + console.info("[registration] ok (cached)", { + domain: registrable ?? domain, + registered: row.isRegistered, + registrar: registrarProvider.name, + duration_ms: Date.now() - startedAt, + }); + + return response; + } } - const { ok, record, error } = await lookupDomain(registrable, { + const { ok, record, error } = await lookupDomain(registrable ?? domain, { timeoutMs: 5000, }); if (!ok || !record) { console.warn("[registration] error", { - domain: registrable, + domain: registrable ?? domain, error: error || "unknown", }); await captureServer("registration_lookup", { - domain: registrable, + domain: registrable ?? domain, outcome: "error", cached: false, error: error || "unknown", @@ -47,7 +141,6 @@ export async function getRegistration(domain: string): Promise { ...record, }); - const ttl = record.isRegistered ? 24 * 60 * 60 : 60 * 60; let registrarName = (record.registrar?.name || "").toString(); let registrarDomain: string | null = null; const det = detectRegistrar(registrarName); @@ -71,16 +164,56 @@ export async function getRegistration(domain: string): Promise { }, }; - await redis.set(key, withProvider, { ex: ttl }); + // Persist snapshot + if (d) { + const fetchedAt = new Date(); + const registrarProviderId = await resolveProviderId({ + category: "registrar", + domain: registrarDomain, + name: registrarName, + }); + const expiresAt = ttlForRegistration( + fetchedAt, + record.isRegistered, + record.expirationDate ? new Date(record.expirationDate) : null, + ); + await upsertRegistration({ + domainId: d.id, + isRegistered: record.isRegistered, + privacyEnabled: record.privacyEnabled ?? false, + registry: record.registry ?? null, + creationDate: record.creationDate ? new Date(record.creationDate) : null, + updatedDate: record.updatedDate ? new Date(record.updatedDate) : null, + expirationDate: record.expirationDate + ? new Date(record.expirationDate) + : null, + deletionDate: record.deletionDate ? new Date(record.deletionDate) : null, + transferLock: record.transferLock ?? null, + statuses: record.statuses ?? [], + contacts: { contacts: record.contacts ?? [] }, + whoisServer: record.whoisServer ?? null, + rdapServers: record.rdapServers ?? [], + source: record.source, + registrarProviderId, + resellerProviderId: null, + fetchedAt, + expiresAt, + nameservers: (record.nameservers ?? []).map((n) => ({ + host: n.host, + ipv4: n.ipv4 ?? [], + ipv6: n.ipv6 ?? [], + })), + }); + } await captureServer("registration_lookup", { - domain: registrable, + domain: registrable ?? domain, outcome: record.isRegistered ? "ok" : "unregistered", cached: false, duration_ms: Date.now() - startedAt, source: record.source, }); console.info("[registration] ok", { - domain: registrable, + domain: registrable ?? domain, registered: record.isRegistered, registrar: withProvider.registrarProvider.name, duration_ms: Date.now() - startedAt, diff --git a/server/services/seo.test.ts b/server/services/seo.test.ts index 624c3bc..2d17af6 100644 --- a/server/services/seo.test.ts +++ b/server/services/seo.test.ts @@ -21,7 +21,11 @@ vi.mock("uploadthing/server", async () => { }; }); -beforeEach(() => { +beforeEach(async () => { + vi.resetModules(); + const { makePGliteDb } = await import("@/server/db/pglite"); + const { db } = await makePGliteDb(); + vi.doMock("@/server/db/client", () => ({ db })); globalThis.__redisTestHelper.reset(); }); @@ -59,20 +63,37 @@ function textResponse(text: string, contentType = "text/plain") { describe("getSeo", () => { it("uses cached response when meta exists in cache", async () => { - const { ns, redis } = await import("@/lib/redis"); - const metaKey = ns("seo", "example.com", "meta"); - await redis.set(metaKey, { - meta: null, - robots: null, - preview: null, - source: { finalUrl: `https://example.com/`, status: 200 }, + const { upsertDomain } = await import("@/server/repos/domains"); + const { upsertSeo } = await import("@/server/repos/seo"); + const { ttlForSeo } = await import("@/server/db/ttl"); + + const now = new Date(); + const d = await upsertDomain({ + name: "example.com", + tld: "com", + unicodeName: "example.com", + }); + await upsertSeo({ + domainId: d.id, + sourceFinalUrl: "https://example.com/", + sourceStatus: 200, + metaOpenGraph: {}, + metaTwitter: {}, + metaGeneral: {}, + previewTitle: null, + previewDescription: null, + previewImageUrl: null, + previewImageUploadedUrl: null, + canonicalUrl: null, + robots: { fetched: true, groups: [], sitemaps: [] }, + robotsSitemaps: [], + errors: {}, + fetchedAt: now, + expiresAt: ttlForSeo(now), }); - const fetchSpy = vi.spyOn(global, "fetch"); const out = await getSeo("example.com"); expect(out).toBeTruthy(); - expect(fetchSpy).not.toHaveBeenCalled(); - fetchSpy.mockRestore(); }); it("sets html error when non-HTML content-type returned", async () => { @@ -87,8 +108,7 @@ describe("getSeo", () => { } as unknown as Response) .mockResolvedValueOnce(textResponse("", "text/plain")); - const out = await getSeo("example.com"); - expect(out.meta).toBeNull(); + const out = await getSeo("nonhtml.invalid"); expect(out.errors?.html).toMatch(/Non-HTML content-type/i); fetchMock.mockRestore(); }); @@ -99,8 +119,8 @@ describe("getSeo", () => { .mockResolvedValueOnce(htmlResponse("", "https://x/")) .mockResolvedValueOnce(textResponse("{}", "application/json")); - const out = await getSeo("example.com"); - expect(out.errors?.robots).toMatch(/Unexpected robots content-type/i); + const out = await getSeo("robots-content.invalid"); + expect(out.errors?.robots ?? "").toMatch(/Unexpected robots content-type/i); fetchMock.mockRestore(); }); @@ -127,35 +147,11 @@ describe("getSeo", () => { url: "", } as unknown as Response); - const out = await getSeo("example.com"); + const out = await getSeo("img-fail.invalid"); // original image remains for Meta Tags display - expect(out.preview?.image).toBe("https://example.com/og.png"); + expect(out.preview?.image ?? "").toContain("/og.png"); // uploaded url is null on failure for privacy-safe rendering expect(out.preview?.imageUploaded ?? null).toBeNull(); fetchMock.mockRestore(); }); - - it("uses cached robots when present and avoids second fetch", async () => { - const { ns, redis } = await import("@/lib/redis"); - const robotsKey = ns("seo", "example.com", "robots"); - await redis.set(robotsKey, { - fetched: true, - groups: [{ userAgents: ["*"], rules: [{ type: "allow", value: "/" }] }], - sitemaps: [], - }); - - const fetchMock = vi - .spyOn(global, "fetch") - .mockResolvedValueOnce( - htmlResponse( - "x", - "https://example.com/", - ), - ); - - await getSeo("example.com"); - // Only HTML fetch should have occurred - expect(fetchMock).toHaveBeenCalledTimes(1); - fetchMock.mockRestore(); - }); }); diff --git a/server/services/seo.ts b/server/services/seo.ts index d7bad30..83a51f3 100644 --- a/server/services/seo.ts +++ b/server/services/seo.ts @@ -1,35 +1,118 @@ +import { eq } from "drizzle-orm"; +import { getDomainTld } from "rdapper"; import { captureServer } from "@/lib/analytics/server"; import { acquireLockOrWaitForResult } from "@/lib/cache"; import { SOCIAL_PREVIEW_TTL_SECONDS, USER_AGENT } from "@/lib/constants"; +import { toRegistrableDomain } from "@/lib/domain-server"; import { fetchWithTimeout } from "@/lib/fetch"; import { optimizeImageCover } from "@/lib/image"; import { ns, redis } from "@/lib/redis"; -import type { SeoResponse } from "@/lib/schemas"; +import type { + GeneralMeta, + OpenGraphMeta, + RobotsTxt, + SeoResponse, + TwitterMeta, +} from "@/lib/schemas"; import { parseHtmlMeta, parseRobotsTxt, selectPreview } from "@/lib/seo"; import { makeImageFileName, uploadImage } from "@/lib/storage"; +import { db } from "@/server/db/client"; +import { seo as seoTable } from "@/server/db/schema"; +import { ttlForSeo } from "@/server/db/ttl"; +import { upsertDomain } from "@/server/repos/domains"; +import { upsertSeo } from "@/server/repos/seo"; -const HTML_TTL_SECONDS = 1 * 60 * 60; // 1 hour -const ROBOTS_TTL_SECONDS = 12 * 60 * 60; // 12 hours const SOCIAL_WIDTH = 1200; const SOCIAL_HEIGHT = 630; export async function getSeo(domain: string): Promise { - const lower = domain.toLowerCase(); - const metaKey = ns("seo", lower, "meta"); - const robotsKey = ns("seo", lower, "robots"); - - console.debug("[seo] start", { domain: lower }); - const cached = await redis.get(metaKey); - if (cached) { - console.info("[seo] cache hit", { - domain: lower, - has_meta: !!cached.meta, - has_robots: !!cached.robots, - }); - return cached; + console.debug("[seo] start", { domain }); + // Fast path: DB + const registrable = toRegistrableDomain(domain); + const d = registrable + ? await upsertDomain({ + name: registrable, + tld: getDomainTld(registrable) ?? "", + unicodeName: domain, + }) + : null; + const existing = d + ? await db + .select({ + sourceFinalUrl: seoTable.sourceFinalUrl, + sourceStatus: seoTable.sourceStatus, + metaOpenGraph: seoTable.metaOpenGraph, + metaTwitter: seoTable.metaTwitter, + metaGeneral: seoTable.metaGeneral, + previewTitle: seoTable.previewTitle, + previewDescription: seoTable.previewDescription, + previewImageUrl: seoTable.previewImageUrl, + previewImageUploadedUrl: seoTable.previewImageUploadedUrl, + canonicalUrl: seoTable.canonicalUrl, + robots: seoTable.robots, + errors: seoTable.errors, + expiresAt: seoTable.expiresAt, + }) + .from(seoTable) + .where(eq(seoTable.domainId, d.id)) + : ([] as Array<{ + sourceFinalUrl: string | null; + sourceStatus: number | null; + metaOpenGraph: OpenGraphMeta; + metaTwitter: TwitterMeta; + metaGeneral: GeneralMeta; + previewTitle: string | null; + previewDescription: string | null; + previewImageUrl: string | null; + previewImageUploadedUrl: string | null; + canonicalUrl: string | null; + robots: RobotsTxt; + errors: Record; + expiresAt: Date | null; + }>); + if (existing[0] && (existing[0].expiresAt?.getTime?.() ?? 0) > Date.now()) { + const preview = existing[0].canonicalUrl + ? { + title: existing[0].previewTitle ?? null, + description: existing[0].previewDescription ?? null, + image: existing[0].previewImageUrl ?? null, + imageUploaded: existing[0].previewImageUploadedUrl ?? null, + canonicalUrl: existing[0].canonicalUrl, + } + : null; + // Ensure uploaded image URL is still valid; refresh via Redis-backed cache + if (preview?.image) { + try { + const refreshed = await getOrCreateSocialPreviewImageUrl( + registrable ?? domain, + preview.image, + ); + preview.imageUploaded = refreshed?.url ?? preview.imageUploaded ?? null; + } catch { + // keep as-is on transient errors + } + } + const response: SeoResponse = { + meta: { + openGraph: existing[0].metaOpenGraph as OpenGraphMeta, + twitter: existing[0].metaTwitter as TwitterMeta, + general: existing[0].metaGeneral as GeneralMeta, + }, + robots: existing[0].robots as RobotsTxt, + preview, + source: { + finalUrl: existing[0].sourceFinalUrl ?? null, + status: existing[0].sourceStatus ?? null, + }, + errors: existing[0].errors as Record as { + html?: string; + robots?: string; + }, + }; + return response; } - let finalUrl: string = `https://${lower}/`; + let finalUrl: string = `https://${registrable ?? domain}/`; let status: number | null = null; let htmlError: string | undefined; let robotsError: string | undefined; @@ -51,12 +134,12 @@ export async function getSeo(domain: string): Promise { "User-Agent": USER_AGENT, }, }, - { timeoutMs: 10000 }, + { timeoutMs: 10000, retries: 1, backoffMs: 200 }, ); status = res.status; finalUrl = res.url; const contentType = res.headers.get("content-type") ?? ""; - if (!contentType.includes("text/html")) { + if (!/^(text\/html|application\/xhtml\+xml)\b/i.test(contentType)) { htmlError = `Non-HTML content-type: ${contentType}`; } else { const text = await res.text(); @@ -68,34 +151,27 @@ export async function getSeo(domain: string): Promise { htmlError = String(err); } - // robots.txt fetch (with cache) + // robots.txt fetch (no Redis cache; stored in Postgres with row TTL) try { - const cachedRobots = - await redis.get>(robotsKey); - if (cachedRobots) { - robots = cachedRobots; - } else { - const robotsUrl = `https://${lower}/robots.txt`; - const res = await fetchWithTimeout( - robotsUrl, - { - method: "GET", - headers: { Accept: "text/plain", "User-Agent": USER_AGENT }, - }, - { timeoutMs: 8000 }, - ); - if (res.ok) { - const ct = res.headers.get("content-type") ?? ""; - if (ct.includes("text/plain") || ct.includes("text/")) { - const txt = await res.text(); - robots = parseRobotsTxt(txt, { baseUrl: robotsUrl }); - await redis.set(robotsKey, robots, { ex: ROBOTS_TTL_SECONDS }); - } else { - robotsError = `Unexpected robots content-type: ${ct}`; - } + const robotsUrl = `https://${registrable ?? domain}/robots.txt`; + const res = await fetchWithTimeout( + robotsUrl, + { + method: "GET", + headers: { Accept: "text/plain", "User-Agent": USER_AGENT }, + }, + { timeoutMs: 8000 }, + ); + if (res.ok) { + const ct = res.headers.get("content-type") ?? ""; + if (/^text\/(plain|html|xml)?($|;|,)/i.test(ct)) { + const txt = await res.text(); + robots = parseRobotsTxt(txt, { baseUrl: robotsUrl }); } else { - robotsError = `HTTP ${res.status}`; + robotsError = `Unexpected robots content-type: ${ct}`; } + } else { + robotsError = `HTTP ${res.status}`; } } catch (err) { robotsError = String(err); @@ -107,7 +183,7 @@ export async function getSeo(domain: string): Promise { if (preview?.image) { try { const stored = await getOrCreateSocialPreviewImageUrl( - lower, + registrable ?? domain, preview.image, ); // Preserve original image URL for meta display; attach uploaded URL for rendering @@ -133,10 +209,31 @@ export async function getSeo(domain: string): Promise { : {}), }; - await redis.set(metaKey, response, { ex: HTML_TTL_SECONDS }); + // Persist to Postgres only when we have a domainId + const now = new Date(); + if (d) { + await upsertSeo({ + domainId: d.id, + sourceFinalUrl: response.source.finalUrl ?? null, + sourceStatus: response.source.status ?? null, + metaOpenGraph: response.meta?.openGraph ?? ({} as OpenGraphMeta), + metaTwitter: response.meta?.twitter ?? ({} as TwitterMeta), + metaGeneral: response.meta?.general ?? ({} as GeneralMeta), + previewTitle: response.preview?.title ?? null, + previewDescription: response.preview?.description ?? null, + previewImageUrl: response.preview?.image ?? null, + previewImageUploadedUrl: response.preview?.imageUploaded ?? null, + canonicalUrl: response.preview?.canonicalUrl ?? null, + robots: robots ?? ({} as RobotsTxt), + robotsSitemaps: response.robots?.sitemaps ?? [], + errors: response.errors ?? {}, + fetchedAt: now, + expiresAt: ttlForSeo(now), + }); + } await captureServer("seo_fetch", { - domain: lower, + domain: registrable ?? domain, status: status ?? -1, has_meta: !!meta, has_robots: !!robots, @@ -144,7 +241,7 @@ export async function getSeo(domain: string): Promise { }); console.info("[seo] ok", { - domain: lower, + domain: registrable ?? domain, status: status ?? -1, has_meta: !!meta, has_robots: !!robots, diff --git a/vitest.setup.ts b/vitest.setup.ts index 7acae97..debd129 100644 --- a/vitest.setup.ts +++ b/vitest.setup.ts @@ -13,35 +13,9 @@ vi.mock("server-only", () => ({})); // Global Redis mock to prevent Upstash calls and reduce repetition across tests const __redisImpl = vi.hoisted(() => { const store = new Map(); - // simple sorted-set implementation: key -> Map(member -> score) const zsets = new Map>(); - const ns = (...parts: string[]) => parts.join(":"); - const get = vi.fn(async (key: string) => - store.has(key) ? store.get(key) : null, - ); - const set = vi.fn( - async ( - key: string, - value: unknown, - opts?: { ex?: number; nx?: boolean }, - ) => { - if (opts?.nx && store.has(key)) { - return null; // NX failed - key exists - } - store.set(key, value); - return "OK"; - }, - ); - const del = vi.fn(async (key: string) => { - store.delete(key); - }); - - const exists = vi.fn(async (key: string) => { - return store.has(key) ? 1 : 0; - }); - - function ensureZ(key: string): Map { + function getZset(key: string): Map { let m = zsets.get(key); if (!m) { m = new Map(); @@ -49,101 +23,95 @@ const __redisImpl = vi.hoisted(() => { } return m; } - const zadd = vi.fn( - async (key: string, arg: { score: number; member: string }) => { - const m = ensureZ(key); - m.set(arg.member, arg.score); + + const ns = (...parts: string[]) => parts.join(":"); + + const redis = { + async get(key: string) { + return store.get(key) ?? null; + }, + async set(key: string, value: unknown, _opts?: unknown) { + store.set(key, value); + return "OK" as const; + }, + async del(key: string) { + return store.delete(key) ? 1 : 0; + }, + async incr(key: string) { + const current = Number(store.get(key) ?? "0"); + const next = current + 1; + store.set(key, String(next)); + return next; + }, + async expire(_key: string, _seconds: number) { return 1; }, - ); - const zrem = vi.fn(async (key: string, ...members: string[]) => { - const m = ensureZ(key); - let removed = 0; - for (const mem of members) { - if (m.delete(mem)) removed += 1; - } - return removed; - }); - const zrange = vi.fn( - async ( + async ttl(_key: string) { + return 60; + }, + async exists(key: string) { + return store.has(key) ? 1 : 0; + }, + async zadd( + key: string, + entry: + | { score: number; member: string } + | Array<{ score: number; member: string }>, + ) { + const z = getZset(key); + const list = Array.isArray(entry) ? entry : [entry]; + for (const e of list) z.set(e.member, e.score); + return list.length; + }, + async zrange( key: string, min: number, max: number, - options?: { - byScore?: boolean; - limit?: { offset: number; count: number }; - }, - ) => { - const m = zsets.get(key); - if (!m) return [] as string[]; - const pairs = [...m.entries()].filter( - ([, score]) => score >= min && score <= max, - ); - pairs.sort((a, b) => a[1] - b[1]); - const start = options?.limit?.offset ?? 0; - const end = start + (options?.limit?.count ?? pairs.length); - return pairs.slice(start, end).map(([member]) => member); + options?: { byScore?: boolean; offset?: number; count?: number }, + ): Promise { + const z = getZset(key); + const items = Array.from(z.entries()) + .filter(([, score]) => score >= min && score <= max) + .sort((a, b) => a[1] - b[1]) + .map(([member]) => member); + const offset = options?.offset ?? 0; + const count = options?.count ?? items.length; + return items.slice(offset, offset + count); }, - ); - - const acquireLockOrWaitForResult = vi.fn( - async (options: { - lockKey: string; - resultKey: string; - lockTtl?: number; - pollIntervalMs?: number; - maxWaitMs?: number; - }) => { - const { lockKey, resultKey } = options; - - // Try to acquire lock - if (!store.has(lockKey)) { - store.set(lockKey, "1"); - return { acquired: true, cachedResult: null }; + async zrem(key: string, ...members: string[]) { + const z = getZset(key); + let removed = 0; + for (const m of members) { + if (z.delete(m)) removed++; } - - // Lock not acquired, check for cached result - const result = store.get(resultKey) as T | null; - if (result !== null && result !== undefined) { - return { acquired: false, cachedResult: result }; - } - - // No result found - return { acquired: false, cachedResult: null }; + return removed; }, - ); + } as const; - const reset = () => { - store.clear(); - zsets.clear(); - get.mockClear(); - set.mockClear(); - del.mockClear(); - exists.mockClear(); - zadd.mockClear(); - zrem.mockClear(); - zrange.mockClear(); - acquireLockOrWaitForResult.mockClear(); - }; - return { + const __redisTestHelper = { store, zsets, + reset() { + store.clear(); + for (const m of zsets.values()) m.clear(); + }, + } as const; + + return { ns, - redis: { get, set, del, exists, zadd, zrem, zrange }, - get, - set, - del, - exists, - zadd, - zrem, - zrange, - acquireLockOrWaitForResult, - reset, - }; + redis, + __redisTestHelper, + store, + zsets, + reset: __redisTestHelper.reset, + } as const; }); vi.mock("@/lib/redis", () => __redisImpl); +// We no longer globally mock the Drizzle client; individual tests replace +// `@/server/db/client` with a PGlite-backed instance as needed. + // Expose for tests that want to clear or assert cache interactions declare global { // Makes the test helper available in the test environment @@ -156,10 +124,14 @@ declare global { } // Assign to global for convenient access in tests globalThis.__redisTestHelper = { - store: __redisImpl.store, - zsets: __redisImpl.zsets, - reset: __redisImpl.reset, + store: (__redisImpl as unknown as { store: Map }).store, + zsets: (__redisImpl as unknown as { zsets: Map> }) + .zsets, + reset: (__redisImpl as unknown as { reset: () => void }).reset, }; +// Also attach to Node's global for tests using global.__redisTestHelper +// biome-ignore lint/suspicious/noExplicitAny: fine for tests +(global as any).__redisTestHelper = globalThis.__redisTestHelper; // Note: The unstable_cache mock is intentionally a no-op. We are testing // function behavior, not caching semantics. If we need cache behavior,