From 5495b94924922774aa4b858da4f566a5de609750 Mon Sep 17 00:00:00 2001 From: Leon Serfaty <80597822+silkoserfo@users.noreply.github.com> Date: Fri, 3 Jul 2026 04:45:24 -0400 Subject: [PATCH] Deploy on DigitalOcean App Platform (GitHub-source build) + consolidate audit-fixes Deploy config: - .do/app.yaml: build the Dockerfile directly from GitHub (deploy_on_push) instead of a pre-built DOCR image; NEXT_PUBLIC_* set RUN_AND_BUILD_TIME with the propertymanagement.network domain so they bake into the client bundle; add custom domains block (apex + www); wire Sentry DSN (server + browser). Included pending work from the audit-fixes branch: - AI provider abstraction (OpenAI/Anthropic, admin-selectable; Anthropic default) - Per-landlord e-signature (DocuSign OAuth + Dropbox Sign) + migration 0010 - Outbound webhooks / Zapier integration - PayPal removal (Stripe-only billing) - Storage hardening (fail-loud when Spaces unconfigured), security fixes Verified: full production Docker build (same build-args as DO) passes clean. Co-Authored-By: Claude Opus 4.8 --- .do/app.yaml | 138 +- .env.example | 56 +- .env.production.example | 43 +- .gitignore | 3 + DIGITALOCEAN.md | 13 +- Dockerfile | 8 + README.md | 16 +- app/(admin)/admin/system/page.tsx | 17 +- app/(auth)/forgot-password/page.tsx | 6 + app/(auth)/login/page.tsx | 6 + app/(dashboard)/leases/[leaseId]/page.tsx | 32 +- app/(dashboard)/settings/billing/page.tsx | 24 +- .../settings/integrations/page.tsx | 51 +- app/actions/admin.ts | 20 + app/actions/auth.ts | 7 +- app/actions/esign.ts | 64 +- app/api/ai/ask/route.ts | 32 +- app/api/ai/maintenance-summary/route.ts | 16 +- app/api/ai/predictions/route.ts | 15 +- app/api/ai/recommendations/route.ts | 15 +- app/api/ai/rent-receipt/route.ts | 16 +- app/api/documents/[id]/route.ts | 2 +- app/api/documents/route.ts | 31 +- app/api/esign/[provider]/callback/route.ts | 48 + app/api/esign/[provider]/connect/route.ts | 49 + app/api/follow-ups/run/route.ts | 8 +- .../integrations/[provider]/callback/route.ts | 38 +- .../integrations/[provider]/connect/route.ts | 18 +- app/api/paypal/cancel/route.ts | 32 - app/api/paypal/checkout/route.ts | 73 - app/api/paypal/return/route.ts | 52 - app/api/paypal/webhook/route.ts | 89 - app/api/profile/route.ts | 9 + app/api/upload/route.ts | 14 +- app/global-error.tsx | 2 + app/manifest.ts | 16 +- app/robots.ts | 44 + app/robots.txt/route.ts | 21 - app/sitemap.ts | 45 +- components/admin/ai-provider-toggle.tsx | 119 + components/dashboard/esign-integrations.tsx | 268 ++ components/forms/checkout-button.tsx | 37 +- components/forms/esign-lease.tsx | 32 +- components/forms/lease-document.tsx | 99 + components/forms/paypal-cancel-button.tsx | 32 - components/marketing/structured-data.tsx | 25 +- instrumentation-client.ts | 21 + instrumentation.ts | 27 +- lib/accounting/state.ts | 37 +- lib/admin/audit.ts | 1 + lib/ai/anthropic.ts | 15 + lib/ai/client.ts | 11 + lib/ai/provider.ts | 146 + lib/db/admin-queries.ts | 1 + lib/db/migrations/0010_esign_connections.sql | 17 + lib/db/migrations/meta/0010_snapshot.json | 3548 +++++++++++++++++ lib/db/migrations/meta/_journal.json | 7 + lib/db/schema.ts | 26 + lib/esign/credentials.ts | 104 + lib/esign/docusign.ts | 162 +- lib/esign/dropbox-sign.ts | 123 +- lib/esign/index.ts | 103 +- lib/esign/registry.ts | 21 + lib/esign/state.ts | 39 + lib/esign/types.ts | 78 +- lib/paypal/checkout.ts | 123 - lib/paypal/client.ts | 57 - lib/paypal/fulfill.ts | 53 - lib/paypal/plans.ts | 28 - lib/paypal/webhook.ts | 36 - lib/storage.ts | 82 +- next.config.ts | 15 +- package-lock.json | 1929 ++++++++- package.json | 2 + proxy.ts | 80 +- public/file.svg | 1 - public/globe.svg | 1 - public/icon-192.png | Bin 0 -> 23402 bytes public/icon-512.png | Bin 0 -> 139564 bytes public/icon-maskable-512.png | Bin 0 -> 54393 bytes public/next.svg | 1 - public/vercel.svg | 1 - public/window.svg | 1 - scripts/paypal-setup-plans.mjs | 102 - sentry.edge.config.ts | 13 + sentry.server.config.ts | 16 + 86 files changed, 7647 insertions(+), 1182 deletions(-) create mode 100644 app/api/esign/[provider]/callback/route.ts create mode 100644 app/api/esign/[provider]/connect/route.ts delete mode 100644 app/api/paypal/cancel/route.ts delete mode 100644 app/api/paypal/checkout/route.ts delete mode 100644 app/api/paypal/return/route.ts delete mode 100644 app/api/paypal/webhook/route.ts create mode 100644 app/robots.ts delete mode 100644 app/robots.txt/route.ts create mode 100644 components/admin/ai-provider-toggle.tsx create mode 100644 components/dashboard/esign-integrations.tsx create mode 100644 components/forms/lease-document.tsx delete mode 100644 components/forms/paypal-cancel-button.tsx create mode 100644 instrumentation-client.ts create mode 100644 lib/ai/anthropic.ts create mode 100644 lib/ai/provider.ts create mode 100644 lib/db/migrations/0010_esign_connections.sql create mode 100644 lib/db/migrations/meta/0010_snapshot.json create mode 100644 lib/esign/credentials.ts create mode 100644 lib/esign/registry.ts create mode 100644 lib/esign/state.ts delete mode 100644 lib/paypal/checkout.ts delete mode 100644 lib/paypal/client.ts delete mode 100644 lib/paypal/fulfill.ts delete mode 100644 lib/paypal/plans.ts delete mode 100644 lib/paypal/webhook.ts delete mode 100644 public/file.svg delete mode 100644 public/globe.svg create mode 100644 public/icon-192.png create mode 100644 public/icon-512.png create mode 100644 public/icon-maskable-512.png delete mode 100644 public/next.svg delete mode 100644 public/vercel.svg delete mode 100644 public/window.svg delete mode 100644 scripts/paypal-setup-plans.mjs create mode 100644 sentry.edge.config.ts create mode 100644 sentry.server.config.ts diff --git a/.do/app.yaml b/.do/app.yaml index 907a92b..ac7af51 100644 --- a/.do/app.yaml +++ b/.do/app.yaml @@ -1,30 +1,28 @@ # ───────────────────────────────────────────────────────────────────────────── # DigitalOcean App Platform spec — Property Management Network # -# Deploy: doctl apps create --spec .do/app.yaml +# Deploy: doctl apps create --spec .do/app.yaml (or the DO MCP apps-create) # Update: doctl apps update --spec .do/app.yaml # -# SOURCE: image-based from DigitalOcean Container Registry (DOCR). The app's git -# lives on self-hosted Gitea, which App Platform cannot pull, so we build the -# Docker image ourselves and push it to DOCR. See DIGITALOCEAN.md for the full -# build/push/deploy walkthrough. +# SOURCE: App Platform builds the Dockerfile directly from GitHub +# (github.com/silkoserfo/property-management-network). Pushes to `main` +# auto-redeploy (deploy_on_push). No DOCR image build/push needed. # # SECRETS: values marked `type: SECRET` are placeholders — set the real values in -# the App Platform dashboard (App → Settings → Environment Variables) or via -# `doctl`. Never commit real secrets to this file. +# the App Platform dashboard (App → Settings → Environment Variables) or via the +# create spec. Never commit real secrets to this file. # ───────────────────────────────────────────────────────────────────────────── name: property-management-network region: nyc services: - name: web - # Pre-built image pushed to DOCR (repository must exist in your registry). - image: - registry_type: DOCR - repository: property-management-network - tag: latest - deploy_on_push: - enabled: true + # Built by App Platform from GitHub using the repo Dockerfile. + github: + repo: silkoserfo/property-management-network + branch: main + deploy_on_push: true + dockerfile_path: Dockerfile instance_count: 1 instance_size_slug: apps-s-1vcpu-1gb http_port: 3000 @@ -37,32 +35,35 @@ services: failure_threshold: 3 envs: # ── App URLs ────────────────────────────────────────────────────────── - # ${APP_URL} resolves to the app's public URL at runtime. NOTE: the client - # bundle bakes NEXT_PUBLIC_APP_URL at *image build* time (see Dockerfile / - # DIGITALOCEAN.md), so build the image with the same URL you serve on. + # NEXT_PUBLIC_* are inlined into the client bundle at BUILD time, so they + # must be RUN_AND_BUILD_TIME with the literal domain we serve on. - key: NEXT_PUBLIC_APP_URL - scope: RUN_TIME - value: ${APP_URL} + scope: RUN_AND_BUILD_TIME + value: https://propertymanagement.network - key: BETTER_AUTH_URL scope: RUN_TIME - value: ${APP_URL} + value: https://propertymanagement.network - key: NEXT_PUBLIC_APP_NAME - scope: RUN_TIME + scope: RUN_AND_BUILD_TIME value: Property Management Network - # ── Database (managed Postgres — use the PRIVATE host; see DIGITALOCEAN.md) ── + # ── Admin & auth policy ─────────────────────────────────────────────── + - key: ADMIN_EMAILS + scope: RUN_TIME + value: leon@phluit.com + - key: REQUIRE_EMAIL_VERIFICATION + scope: RUN_TIME + value: "true" + + # ── Database (managed Postgres — PRIVATE host, direct port 25060) ── - key: DATABASE_URL scope: RUN_TIME type: SECRET value: REPLACE_IN_DASHBOARD - # Verified TLS (encrypted + certificate-checked). DO Managed Postgres uses - # a CA that isn't in the system trust store, so paste the cluster's CA cert - # into DATABASE_CA: DO control panel → Database → Connection Details → - # "Download CA certificate", then paste its PEM contents as the DATABASE_CA - # secret in the App Platform dashboard. Without a valid CA the app will - # refuse to connect (fail loud) rather than run unverified. - # Emergency fallback ONLY (not for production): DATABASE_SSL=no-verify is - # encrypted but does NOT verify the server certificate. + # Verified TLS: DO's Managed Postgres CA isn't in the system trust store, so + # paste the cluster CA PEM (repo root ca-certificate.crt) into DATABASE_CA. + # With `require` + a valid CA the app connects verified; without a valid CA + # it fails loud rather than run unverified. - key: DATABASE_SSL scope: RUN_TIME value: require @@ -70,8 +71,7 @@ services: scope: RUN_TIME type: SECRET value: REPLACE_IN_DASHBOARD - # Schema is migrated out-of-band (as doadmin), NOT on boot — the app user - # intentionally lacks DDL rights. Keep this false; run migrations manually. + # Schema is migrated out-of-band (as doadmin), NOT on boot. - key: RUN_MIGRATIONS_ON_START scope: RUN_TIME value: "false" @@ -81,14 +81,15 @@ services: scope: RUN_TIME type: SECRET value: REPLACE_IN_DASHBOARD + # Google OAuth (optional — leave blank to disable the Google button). - key: GOOGLE_CLIENT_ID scope: RUN_TIME type: SECRET - value: REPLACE_IN_DASHBOARD + value: "" - key: GOOGLE_CLIENT_SECRET scope: RUN_TIME type: SECRET - value: REPLACE_IN_DASHBOARD + value: "" # ── Stripe ──────────────────────────────────────────────────────────── - key: STRIPE_SECRET_KEY @@ -99,22 +100,21 @@ services: scope: RUN_TIME type: SECRET value: REPLACE_IN_DASHBOARD - # No STRIPE_*_PRICE_ID vars — prices are resolved by lookup key and - # auto-created on first checkout (lib/stripe/prices.ts). Going live only - # needs the two live secrets above + the live publishable key below. - # ── OpenAI ──────────────────────────────────────────────────────────── - - key: OPENAI_API_KEY + # ── AI provider (Anthropic default; OpenAI optional) ────────────────── + - key: ANTHROPIC_API_KEY scope: RUN_TIME type: SECRET value: REPLACE_IN_DASHBOARD + - key: ANTHROPIC_MODEL + scope: RUN_TIME + value: claude-haiku-4-5 + - key: OPENAI_API_KEY + scope: RUN_TIME + type: SECRET + value: "" # ── Email (SMTP — SMTP2GO) ──────────────────────────────────────────── - # The app sends mail via SMTP only (nodemailer). Email is silently skipped - # unless SMTP_HOST + SMTP_USER + SMTP_PASS are all set — password resets, - # email verification, rent/overdue/lease reminders, team invites, and - # payment links all depend on this. EMAIL_FROM is a bare address; the app - # wraps it as "Property Management Network <…>". - key: SMTP_HOST scope: RUN_TIME value: mail.smtp2go.com @@ -133,10 +133,9 @@ services: scope: RUN_TIME value: postmaster@propertymanagement.network - # ── Cloudflare Turnstile (site key is public; baked into the client bundle - # at image build time — keep it in sync when you build) ── + # ── Cloudflare Turnstile (site key public; baked at build time) ── - key: NEXT_PUBLIC_TURNSTILE_SITE_KEY - scope: RUN_TIME + scope: RUN_AND_BUILD_TIME value: 0x4AAAAAADuDQverznfv1a60 - key: TURNSTILE_SECRET_KEY scope: RUN_TIME @@ -165,8 +164,53 @@ services: scope: RUN_TIME value: https://nyc3.cdn.digitaloceanspaces.com + # ── Accounting sync (optional — per-landlord QuickBooks / Xero OAuth) ── + - key: QBO_CLIENT_ID + scope: RUN_TIME + type: SECRET + value: "" + - key: QBO_CLIENT_SECRET + scope: RUN_TIME + type: SECRET + value: "" + - key: QBO_ENVIRONMENT + scope: RUN_TIME + value: production + - key: XERO_CLIENT_ID + scope: RUN_TIME + type: SECRET + value: "" + - key: XERO_CLIENT_SECRET + scope: RUN_TIME + type: SECRET + value: "" + - key: XERO_SALES_ACCOUNT_CODE + scope: RUN_TIME + value: "200" + - key: XERO_EXPENSE_ACCOUNT_CODE + scope: RUN_TIME + value: "400" + + # ── Error monitoring (Sentry — DSN is public; browser DSN baked at build) ── + - key: SENTRY_DSN + scope: RUN_TIME + value: https://ef6aa585a080711e14a855b6cc024e9a@o4509830676873216.ingest.us.sentry.io/4511667160219648 + - key: NEXT_PUBLIC_SENTRY_DSN + scope: RUN_AND_BUILD_TIME + value: https://ef6aa585a080711e14a855b6cc024e9a@o4509830676873216.ingest.us.sentry.io/4511667160219648 + - key: SENTRY_ENVIRONMENT + scope: RUN_TIME + value: production + # ── Cron (Bearer token the DO Function sends to /api/cron/*) ── - key: CRON_SECRET scope: RUN_TIME type: SECRET value: REPLACE_IN_DASHBOARD + +# ── Custom domains (DNS hosted on Cloudflare — set CNAMEs there, DNS-only) ── +domains: + - domain: propertymanagement.network + type: PRIMARY + - domain: www.propertymanagement.network + type: ALIAS diff --git a/.env.example b/.env.example index 70863e0..d15e863 100644 --- a/.env.example +++ b/.env.example @@ -50,24 +50,16 @@ NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_your-publishable-key # auto-creates them on first checkout (lib/stripe/prices.ts), so going live is a # pure key swap. Optionally pre-create the catalog: node scripts/stripe-setup.mjs -# === PAYPAL (optional — alternative subscription checkout) === -# Lets landlords pay for their plan with PayPal alongside Stripe. Leave blank to -# hide the PayPal buttons. Create a REST app at https://developer.paypal.com; -# keep PAYPAL_ENVIRONMENT=sandbox for testing. Create a webhook pointing to -# /api/paypal/webhook and set its id as PAYPAL_WEBHOOK_ID. Generate the -# plan IDs once with `node scripts/paypal-setup-plans.mjs` and paste them below. -PAYPAL_CLIENT_ID= -PAYPAL_SECRET= -PAYPAL_ENVIRONMENT=sandbox -PAYPAL_WEBHOOK_ID= -PAYPAL_PRO_MONTHLY_PLAN_ID= -PAYPAL_PRO_YEARLY_PLAN_ID= -PAYPAL_LANDLORD_MONTHLY_PLAN_ID= -PAYPAL_LANDLORD_YEARLY_PLAN_ID= - -# === AI (OpenAI) === -# Get from: https://platform.openai.com/api-keys +# === AI PROVIDER (OpenAI and/or Anthropic) === +# The active provider is chosen by an admin in Settings → System. Configure the +# key(s) for whichever provider(s) you want available; the app falls back to the +# configured one if the selected provider's key is missing. +# OpenAI — https://platform.openai.com/api-keys OPENAI_API_KEY=sk-your-api-key +# OPENAI_MODEL=gpt-4o-mini +# Anthropic (Claude) — https://console.anthropic.com/settings/keys +ANTHROPIC_API_KEY= +# ANTHROPIC_MODEL=claude-haiku-4-5 # cheapest; use claude-sonnet-5 / claude-opus-4-8 for more capability # === EMAIL (SMTP — e.g. SMTP2GO) === # Any SMTP provider works. Port 465 = implicit SSL; 587/2525 = STARTTLS. @@ -87,16 +79,18 @@ QBO_ENVIRONMENT=sandbox XERO_CLIENT_ID= XERO_CLIENT_SECRET= -# === E-SIGNATURE (optional — DocuSign / Dropbox Sign) === -# Dropbox Sign: API-key auth. Set DROPBOX_SIGN_TEST_MODE=true while testing. -DROPBOX_SIGN_API_KEY= -DROPBOX_SIGN_TEST_MODE=true -# DocuSign: uses a pre-obtained access token (JWT/OAuth). Webhook: DocuSign -# Connect → /api/esign/docusign/webhook ; Dropbox Sign callback → -# /api/esign/dropbox_sign/webhook -DOCUSIGN_ACCESS_TOKEN= -DOCUSIGN_ACCOUNT_ID= -DOCUSIGN_BASE_URI=https://demo.docusign.net +# === E-SIGNATURE (optional — per-landlord: each connects their OWN account) === +# DocuSign: register ONE DocuSign app (integration key) here; each landlord then +# connects their own DocuSign account via OAuth from Settings → Integrations. +# Redirect URI to register in the DocuSign app: /api/esign/docusign/callback +# DOCUSIGN_OAUTH_BASE: account-d.docusign.com (demo) or account.docusign.com (prod). +DOCUSIGN_CLIENT_ID= +DOCUSIGN_CLIENT_SECRET= +DOCUSIGN_OAUTH_BASE=account-d.docusign.com +# Dropbox Sign: no server credentials — landlords paste their own API key in the +# app and set their account callback URL to /api/esign/dropbox_sign/webhook. +# DROPBOX_SIGN_TEST_MODE applies test mode to all outbound requests (optional). +DROPBOX_SIGN_TEST_MODE=false # === APP === NEXT_PUBLIC_APP_URL=http://localhost:3000 @@ -113,6 +107,14 @@ CRON_SECRET=your-random-secret-string NEXT_PUBLIC_UMAMI_SRC=https://fickanalytics.phluit.net/script.js NEXT_PUBLIC_UMAMI_WEBSITE_ID=4066c359-596f-4d0e-9636-c035c2adfbe8 +# === ERROR MONITORING (Sentry — optional) === +# Paste the DSN from your Sentry project (Settings → Client Keys / DSN). It's +# public (ships in the browser bundle). Sentry stays inert until this is set. +NEXT_PUBLIC_SENTRY_DSN= +# Build-time only: uploads source maps for readable stack traces. Create at +# Sentry → Settings → Auth Tokens. Keep secret; leave blank to skip upload. +SENTRY_AUTH_TOKEN= + # === MAPS / GEOCODING (OpenStreetMap — free, no key) === # Property addresses are geocoded on save via OpenStreetMap Nominatim and shown # on a Leaflet map (both keyless & free). Nominatim's policy requires an diff --git a/.env.production.example b/.env.production.example index 4f6b019..f33a8d4 100644 --- a/.env.production.example +++ b/.env.production.example @@ -68,21 +68,6 @@ NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_xxx # first checkout, so going live is ONLY the three values above (live keys + live # webhook secret). Optionally pre-create the catalog: node scripts/stripe-setup.mjs -# === PAYPAL (optional — alternative subscription checkout) === -# Landlords can pay for their plan with PayPal alongside Stripe. Leave blank to -# hide the PayPal buttons. Create a REST app at https://developer.paypal.com and -# set PAYPAL_ENVIRONMENT=live for production. Create a webhook there pointing to -# /api/paypal/webhook and put its id in PAYPAL_WEBHOOK_ID. Generate the -# plan IDs with `node scripts/paypal-setup-plans.mjs`. -PAYPAL_CLIENT_ID= -PAYPAL_SECRET= -PAYPAL_ENVIRONMENT=live -PAYPAL_WEBHOOK_ID= -PAYPAL_PRO_MONTHLY_PLAN_ID= -PAYPAL_PRO_YEARLY_PLAN_ID= -PAYPAL_LANDLORD_MONTHLY_PLAN_ID= -PAYPAL_LANDLORD_YEARLY_PLAN_ID= - # === AI (OpenAI) === OPENAI_API_KEY=sk-xxx @@ -103,17 +88,17 @@ QBO_ENVIRONMENT=production XERO_CLIENT_ID= XERO_CLIENT_SECRET= -# === E-SIGNATURE (optional — DocuSign / Dropbox Sign) === -# Leave blank to hide/disable a provider on the lease page. Configure the -# provider callbacks to point at this app: -# Dropbox Sign callback → /api/esign/dropbox_sign/webhook -# DocuSign Connect → /api/esign/docusign/webhook -# In production set DROPBOX_SIGN_TEST_MODE=false to send legally-binding docs. -DROPBOX_SIGN_API_KEY= +# === E-SIGNATURE (optional — per-landlord: each connects their OWN account) === +# DocuSign: register ONE DocuSign app; landlords connect their own account via +# OAuth from Settings → Integrations. Register this redirect URI in the app: +# /api/esign/docusign/callback +# Use account.docusign.com in production (account-d.docusign.com for demo). +DOCUSIGN_CLIENT_ID= +DOCUSIGN_CLIENT_SECRET= +DOCUSIGN_OAUTH_BASE=account.docusign.com +# Dropbox Sign: no server credentials — landlords paste their own API key and set +# their account callback URL to /api/esign/dropbox_sign/webhook. DROPBOX_SIGN_TEST_MODE=false -DOCUSIGN_ACCESS_TOKEN= -DOCUSIGN_ACCOUNT_ID= -DOCUSIGN_BASE_URI=https://www.docusign.net # === APP (NEXT_PUBLIC_* — also set as Build Variables) === NEXT_PUBLIC_APP_URL=https://propertymanagement.network @@ -128,6 +113,14 @@ GOOGLE_SITE_VERIFICATION= NEXT_PUBLIC_UMAMI_SRC=https://fickanalytics.phluit.net/script.js NEXT_PUBLIC_UMAMI_WEBSITE_ID=4066c359-596f-4d0e-9636-c035c2adfbe8 +# === ERROR MONITORING (Sentry) === +# DSN from your Sentry project (public — inlined in the browser bundle, so set +# it as a Build Variable too). Error monitoring is disabled until this is set. +NEXT_PUBLIC_SENTRY_DSN= +# Build-time secret: uploads source maps so prod stack traces are un-minified. +# Sentry → Settings → Auth Tokens. Set as a Build Variable; leave blank to skip. +SENTRY_AUTH_TOKEN= + # === MAPS / GEOCODING (OpenStreetMap — free, no key) === # Addresses are geocoded via OpenStreetMap Nominatim; the map uses Leaflet + OSM # tiles. No API key or billing. Nominatim REQUIRES an identifying User-Agent — diff --git a/.gitignore b/.gitignore index d466acc..41be56f 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,9 @@ yarn-debug.log* yarn-error.log* .pnpm-debug.log* +# MCP config — contains a DigitalOcean API token; keep local, never commit. +.mcp.json + # env files (can opt-in for committing if needed) .env* !.env.example diff --git a/DIGITALOCEAN.md b/DIGITALOCEAN.md index 6bffabd..57f509a 100644 --- a/DIGITALOCEAN.md +++ b/DIGITALOCEAN.md @@ -94,11 +94,19 @@ docker build \ --build-arg NEXT_PUBLIC_APP_URL=https:// \ --build-arg NEXT_PUBLIC_APP_NAME="Property Management Network" \ --build-arg NEXT_PUBLIC_TURNSTILE_SITE_KEY=0x4AAAAAADuDQverznfv1a60 \ + --build-arg NEXT_PUBLIC_SENTRY_DSN= \ + --build-arg SENTRY_AUTH_TOKEN= \ -t $REG/property-management-network:latest . docker push $REG/property-management-network:latest ``` +> **Sentry:** the browser DSN is baked at build time, so it must be a `--build-arg` +> (setting `NEXT_PUBLIC_SENTRY_DSN` only in the dashboard won't reach the client). The +> server/edge runtimes read `SENTRY_DSN` at runtime (set in the dashboard). Both stay inert +> until a DSN is provided, so it's safe to omit until you're ready. `SENTRY_AUTH_TOKEN` is +> optional and only uploads source maps for readable stack traces. + > First deploy chicken-and-egg: if you don't have a domain yet, deploy once to get the > `*.ondigitalocean.app` URL, then rebuild/push with that URL as `NEXT_PUBLIC_APP_URL`. @@ -114,7 +122,10 @@ Then set every `type: SECRET` value (App → Settings → Environment Variables) `.do/app.yaml` before applying. Secrets to fill: `DATABASE_URL`, `DATABASE_CA`, `BETTER_AUTH_SECRET`, `GOOGLE_CLIENT_ID/SECRET`, `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`, `OPENAI_API_KEY`, `SMTP_USER`, `SMTP_PASS`, `TURNSTILE_SECRET_KEY`, `SPACES_KEY`, -`SPACES_SECRET`, `CRON_SECRET` (plus the Stripe price IDs). Email sends via **SMTP +`SPACES_SECRET`, `CRON_SECRET`. Optional integrations (leave blank to keep hidden): +`QBO_CLIENT_ID/SECRET` + `XERO_CLIENT_ID/SECRET` (accounting), `DOCUSIGN_CLIENT_ID/SECRET` +(e-signature — not yet in the spec; add if used), and `SENTRY_DSN` (error monitoring — +plus the `NEXT_PUBLIC_SENTRY_DSN` build-arg above). Email sends via **SMTP (SMTP2GO)** — `SMTP_HOST`/`SMTP_PORT`/`EMAIL_FROM` ship as non-secret defaults; without `SMTP_USER` + `SMTP_PASS` all outbound email is silently skipped. `${APP_URL}` auto-resolves for `BETTER_AUTH_URL` / `NEXT_PUBLIC_APP_URL` at runtime. diff --git a/Dockerfile b/Dockerfile index 3020201..35d7288 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,9 +24,17 @@ ENV NEXT_TELEMETRY_DISABLED=1 ARG NEXT_PUBLIC_APP_URL ARG NEXT_PUBLIC_APP_NAME="Property Management Network" ARG NEXT_PUBLIC_TURNSTILE_SITE_KEY +# Client-side Sentry DSN — inlined into the browser bundle. Without it, only +# server/edge errors are reported (SENTRY_DSN at runtime); the browser stays inert. +ARG NEXT_PUBLIC_SENTRY_DSN +# Optional: a Sentry auth token uploads source maps for readable stack traces. +# The build still succeeds without it. +ARG SENTRY_AUTH_TOKEN ENV NEXT_PUBLIC_APP_URL=$NEXT_PUBLIC_APP_URL ENV NEXT_PUBLIC_APP_NAME=$NEXT_PUBLIC_APP_NAME ENV NEXT_PUBLIC_TURNSTILE_SITE_KEY=$NEXT_PUBLIC_TURNSTILE_SITE_KEY +ENV NEXT_PUBLIC_SENTRY_DSN=$NEXT_PUBLIC_SENTRY_DSN +ENV SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN COPY --from=deps /app/node_modules ./node_modules COPY . . RUN npm run build diff --git a/README.md b/README.md index b6d531d..67faf89 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Everything is **multi-tenant and team-aware**: each landlord operates on their o **Core operations** - 🏢 **Properties & units** — manage your whole portfolio with live occupancy tracking and a map view (addresses are auto-geocoded). - 👥 **Tenants** — profiles, lease history, payment records, and a private **tenant portal** (token-based, no login required). -- 💵 **Rent tracking** — log payments, send **Stripe or PayPal** payment links, and auto-mark balances overdue with automatic late fees. +- 💵 **Rent tracking** — log payments, send **Stripe** payment links, and auto-mark balances overdue with automatic late fees. - 🔧 **Maintenance** — full status workflow (Open → In Progress → Resolved), with tenant-submitted requests from the portal. - 📄 **Leases** — expiry countdowns, automated 60/30/7-day email alerts, and **e-signature** (DocuSign / Dropbox Sign). - 🧾 **Expenses** — categorized logging with recurring-expense support. @@ -55,7 +55,7 @@ Everything is **multi-tenant and team-aware**: each landlord operates on their o |---|---| | 🌐 **Public REST API** | Versioned `/api/v1` endpoints (properties, tenants, payments, maintenance, webhooks) authenticated with Bearer **API keys**. See `/api-docs`. | | 🪝 **Outbound webhooks / Zapier** | Subscribe to events (`tenant.created`, `payment.paid`, `maintenance.updated`, …). Deliveries are **HMAC-signed**, retried with backoff, and Zapier-compatible via the REST-hook subscribe/unsubscribe pattern. | -| 💳 **Payments** | Stripe (subscriptions + rent payment links) and PayPal. | +| 💳 **Payments** | Stripe (subscriptions + rent payment links). | | 📚 **Accounting sync** | One-way push of income & expenses to **QuickBooks Online** or **Xero** (OAuth). | | ✍️ **E-signature** | Send leases for signature via **DocuSign** or **Dropbox Sign**. | | 🔑 **Auth** | Email/password and Google OAuth (Better Auth). | @@ -73,7 +73,7 @@ Every integration is env-gated: unconfigured providers show a clean “not confi | 🏆 **Landlord** | $59/mo | Unlimited properties, team access, white-label, AI (200/mo) | | ♾️ **Lifetime** | $199 once | Everything in Landlord, forever | -Billing runs through **Stripe** or **PayPal**. Stripe products/prices are resolved by stable lookup keys and auto-created on first checkout, so going live is just an API-key swap — no price IDs to wire up. +Billing runs through **Stripe**. Products/prices are resolved by stable lookup keys and auto-created on first checkout, so going live is just an API-key swap — no price IDs to wire up. --- @@ -86,7 +86,7 @@ Billing runs through **Stripe** or **PayPal**. Stripe products/prices are resolv | Database | PostgreSQL via **Drizzle ORM** | | Auth | Better Auth (email/password + Google OAuth) | | Object storage | DigitalOcean Spaces (S3-compatible, CDN, auth-gated) | -| Payments | Stripe + PayPal | +| Payments | Stripe | | AI | OpenAI (`gpt-4o-mini`) | | Email | SMTP (SMTP2GO) | | Maps | Leaflet + OpenStreetMap / Nominatim geocoding | @@ -113,7 +113,7 @@ Copy the template and fill in your own values: cp .env.example .env.local ``` -`.env.local` holds your database URL, auth secret, and credentials for Stripe/PayPal, OpenAI, SMTP, and object storage. **Every variable is documented inline in `.env.example`**, and the full production reference lives in **[DIGITALOCEAN.md](DIGITALOCEAN.md)**. Never commit real secrets. +`.env.local` holds your database URL, auth secret, and credentials for Stripe, OpenAI, SMTP, and object storage. **Every variable is documented inline in `.env.example`**, and the full production reference lives in **[DIGITALOCEAN.md](DIGITALOCEAN.md)**. Never commit real secrets. ### 3. 🗄️ Run migrations @@ -129,7 +129,7 @@ npm run db:push # push schema directly (quick local prototyping) - **Stripe** — set the API keys, then add a webhook at `https://yourdomain.com/api/stripe/webhook` for `checkout.session.completed`, the `customer.subscription.*` events, `invoice.payment_failed`, and `payment_intent.succeeded`. - **Email** — verify a sending domain with your SMTP provider (e.g. SMTP2GO) and set the `SMTP_*` + `EMAIL_FROM` vars. -- **Google / PayPal / OpenAI / accounting / e-sign** — each is optional and activates once its env vars are present. +- **Google / OpenAI / accounting / e-sign** — each is optional and activates once its env vars are present. ### 5. ▶️ Run locally @@ -158,7 +158,7 @@ app/ ├── api/ │ ├── v1/ # 🌐 Public REST API (Bearer API keys) │ ├── webhooks + cron/ # 🪝 Outbound webhook delivery + scheduled jobs -│ ├── stripe/ paypal/ # 💳 Billing + payment links + provider webhooks +│ ├── stripe/ # 💳 Billing + payment links + provider webhooks │ ├── integrations/ # 📚 QuickBooks / Xero OAuth │ ├── esign/ # ✍️ DocuSign / Dropbox Sign │ └── … # Properties, tenants, rent, maintenance, documents, AI @@ -169,7 +169,7 @@ lib/ ├── auth.ts account.ts # Better Auth + team/account scoping ├── storage.ts # Object storage (Spaces) with local-disk dev fallback ├── webhooks/ # Event catalog, HMAC signing, SSRF guard, delivery -├── stripe/ paypal/ # Billing clients & plans +├── stripe/ # Billing clients & plans ├── accounting/ esign/ # QuickBooks/Xero & DocuSign/Dropbox Sign ├── ai/ # OpenAI client + prompts ├── email/ # SMTP (SMTP2GO) client + HTML templates diff --git a/app/(admin)/admin/system/page.tsx b/app/(admin)/admin/system/page.tsx index d107050..b185eea 100644 --- a/app/(admin)/admin/system/page.tsx +++ b/app/(admin)/admin/system/page.tsx @@ -1,6 +1,8 @@ import { getSystemCounts, getEnvHealth } from "@/lib/db/admin-queries" import { getMaintenanceMode } from "@/lib/settings" +import { aiProviderStatus } from "@/lib/ai/provider" import { MaintenanceToggle } from "@/components/admin/maintenance-toggle" +import { AiProviderToggle } from "@/components/admin/ai-provider-toggle" import { formatDate } from "@/lib/utils" import { Settings, Database, Table2 } from "lucide-react" @@ -13,10 +15,11 @@ function humanize(name: string): string { } export default async function AdminSystemPage() { - const [{ counts, cronLastRun }, env, maintenance] = await Promise.all([ + const [{ counts, cronLastRun }, env, maintenance, aiProvider] = await Promise.all([ getSystemCounts(), Promise.resolve(getEnvHealth()), getMaintenanceMode(), + aiProviderStatus(), ]) return ( @@ -37,6 +40,18 @@ export default async function AdminSystemPage() { /> + {/* AI provider selection */} +
+ +
+
{/* ── Environment configuration ───────────────────────────────── */}
diff --git a/app/(auth)/forgot-password/page.tsx b/app/(auth)/forgot-password/page.tsx index 72d2173..95a6749 100644 --- a/app/(auth)/forgot-password/page.tsx +++ b/app/(auth)/forgot-password/page.tsx @@ -1,8 +1,14 @@ +import type { Metadata } from "next" import Link from "next/link" import { Logo } from "@/components/shared/logo" import { TurnstileWidget } from "@/components/shared/turnstile-widget" import { resetPassword } from "@/app/actions/auth" +export const metadata: Metadata = { + title: "Reset password", + robots: { index: false, follow: true }, +} + export default async function ForgotPasswordPage({ searchParams, }: { diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx index 4ad00ec..4b93b4d 100644 --- a/app/(auth)/login/page.tsx +++ b/app/(auth)/login/page.tsx @@ -1,8 +1,14 @@ +import type { Metadata } from "next" import Link from "next/link" import { Logo } from "@/components/shared/logo" import { TurnstileWidget } from "@/components/shared/turnstile-widget" import { signIn, signInWithGoogle } from "@/app/actions/auth" +export const metadata: Metadata = { + title: "Sign in", + robots: { index: false, follow: true }, +} + export default async function LoginPage({ searchParams, }: { diff --git a/app/(dashboard)/leases/[leaseId]/page.tsx b/app/(dashboard)/leases/[leaseId]/page.tsx index 58c2790..8f97c75 100644 --- a/app/(dashboard)/leases/[leaseId]/page.tsx +++ b/app/(dashboard)/leases/[leaseId]/page.tsx @@ -4,13 +4,16 @@ import { db } from "@/lib/db" import { leases as leasesTable } from "@/lib/db/schema" import { getSessionUser } from "@/lib/session" import { getAccountContext } from "@/lib/account" -import { listAdapters, listRequestsForLease } from "@/lib/esign" +import { listEsignConnections, listRequestsForLease } from "@/lib/esign" import Link from "next/link" -import { FileText, ExternalLink } from "lucide-react" +import { FileText } from "lucide-react" import { formatCurrency, formatDate, daysUntil } from "@/lib/utils" import { cn } from "@/lib/utils" import { LeaseActions } from "@/components/forms/lease-actions" import { EsignLease } from "@/components/forms/esign-lease" +import { LeaseDocument } from "@/components/forms/lease-document" + +const ESIGN_LABEL: Record = { docusign: "DocuSign", dropbox_sign: "Dropbox Sign" } export const metadata = { title: "Lease" } @@ -56,8 +59,12 @@ export default async function LeaseDetailPage({ params }: { params: Promise<{ le if (!lease) notFound() const esignRequests = await listRequestsForLease(ownerId, leaseId) - const esignProviders = listAdapters() - const canSendEsign = ctx.canWrite && !!lease.document_url && !!lease.tenant?.email + const esignConnections = await listEsignConnections(ownerId) + const connectedProviders = esignConnections + .filter((c) => c.status !== "revoked") + .map((c) => ({ id: c.provider, label: ESIGN_LABEL[c.provider] ?? c.provider })) + const canSendEsign = + ctx.canWrite && !!lease.document_url && !!lease.tenant?.email && connectedProviders.length > 0 const esignDisabledReason = !ctx.canWrite ? "You have read-only access." : !lease.document_url @@ -196,24 +203,11 @@ export default async function LeaseDetailPage({ params }: { params: Promise<{ le
- {lease.document_url && ( - -
- - Lease document -
- -
- )} + + searchParams: Promise<{ success?: string; canceled?: string }> }) { const user = await getSessionUser() if (!user) redirect("/login") @@ -72,16 +70,12 @@ export default async function BillingPage({ plan_expires_at: true, stripe_customer_id: true, stripe_subscription_id: true, - paypal_subscription_id: true, - billing_provider: true, }, }) const params = await searchParams const currentPlan = (profile?.plan ?? "starter") as Plan const hasStripeAccount = !!profile?.stripe_customer_id - const isPaypal = profile?.billing_provider === "paypal" || !!profile?.paypal_subscription_id - const paypalEnabled = paypalConfigured() const limits = PLAN_LIMITS[currentPlan] const canBillAnnually = annualEnabled() @@ -113,12 +107,6 @@ export default async function BillingPage({ Checkout canceled — no charge was made. )} - {params.error === "paypal" && ( -
- We couldn't complete your PayPal payment. No charge was made — please try again. -
- )} - {/* Current plan */}
@@ -129,12 +117,9 @@ export default async function BillingPage({

Status: {profile.subscription_status}

)}
- {currentPlan !== "starter" && currentPlan !== "lifetime" && - (isPaypal ? ( - - ) : hasStripeAccount ? ( - - ) : null)} + {hasStripeAccount && currentPlan !== "starter" && currentPlan !== "lifetime" && ( + + )}
@@ -197,7 +182,6 @@ export default async function BillingPage({ label={plan.cta} highlight={plan.highlight} annualAvailable={canBillAnnually && plan.key !== "lifetime"} - paypalEnabled={paypalEnabled} /> )} diff --git a/app/(dashboard)/settings/integrations/page.tsx b/app/(dashboard)/settings/integrations/page.tsx index 8cbc225..ec8591b 100644 --- a/app/(dashboard)/settings/integrations/page.tsx +++ b/app/(dashboard)/settings/integrations/page.tsx @@ -2,7 +2,9 @@ import { redirect } from "next/navigation" import { getSessionUser } from "@/lib/session" import { getAccountContext } from "@/lib/account" import { listProviders, listConnections } from "@/lib/accounting" +import { listEsignAdapters, listEsignConnections } from "@/lib/esign" import { AccountingIntegrations } from "@/components/dashboard/accounting-integrations" +import { EsignIntegrations } from "@/components/dashboard/esign-integrations" export const metadata = { title: "Integrations" } export const dynamic = "force-dynamic" @@ -20,20 +22,45 @@ export default async function IntegrationsPage({ const providers = listProviders() const connections = ctx.isOwner ? await listConnections(ctx.ownerId) : [] + const esignAdapters = listEsignAdapters() + const esignConnections = ctx.isOwner ? await listEsignConnections(ctx.ownerId) : [] + const appUrl = (process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000").replace(/\/+$/, "") + // DocuSign vs Dropbox Sign flash messages are keyed by provider id, so a single + // connected/error param drives whichever card the user just acted on. + const esignFlash = { connected: sp.connected, error: sp.error } + return ( -
-
-

Integrations

-

- Connect your accounting software to automatically push rent income and expenses into your books. -

+
+
+
+

E-signature

+

+ Connect your own DocuSign or Dropbox Sign account to send leases for signature. +

+
+ +
+ +
+
+

Accounting

+

+ Connect your accounting software to automatically push rent income and expenses into your books. +

+
+
-
) } diff --git a/app/actions/admin.ts b/app/actions/admin.ts index d76e732..7e97850 100644 --- a/app/actions/admin.ts +++ b/app/actions/admin.ts @@ -8,6 +8,7 @@ import { z } from "zod" import { getAdminSession } from "@/lib/session" import { logAdminAction } from "@/lib/admin/audit" import { setMaintenanceMode } from "@/lib/settings" +import { setAiProvider, type AiProvider } from "@/lib/ai/provider" import { auth } from "@/lib/auth" import { db } from "@/lib/db" import { profiles, user as userTable } from "@/lib/db/schema" @@ -158,3 +159,22 @@ export async function setSiteMaintenance(enabled: boolean, message?: string) { revalidatePath("/", "layout") return { ok: true } } + +// ── AI provider ─────────────────────────────────────────────────────────────── +// Chooses which LLM provider powers all AI features (OpenAI or Anthropic/Claude), +// persisted in app_settings. Applies immediately to every AI route. +export async function setAiProviderAction(provider: string) { + const a = await guard() + if (provider !== "openai" && provider !== "anthropic") throw new Error("Invalid AI provider") + + await setAiProvider(provider as AiProvider) + + await logAdminAction({ + adminId: a.user.id, + action: "ai_provider", + metadata: { provider }, + }) + + revalidatePath("/admin/system") + return { ok: true } +} diff --git a/app/actions/auth.ts b/app/actions/auth.ts index a6700cb..0e64a44 100644 --- a/app/actions/auth.ts +++ b/app/actions/auth.ts @@ -36,7 +36,12 @@ export async function signUp(formData: FormData) { headers: h, }) } catch (e) { - const msg = e instanceof APIError ? e.message : "Sign up failed" + const raw = e instanceof APIError ? e.message : "Sign up failed" + // Don't reveal that an email is already registered (user enumeration) — the + // "already exists" path must not be distinguishable from other failures. + const msg = /exist|registered|already|taken/i.test(raw) + ? "We couldn't complete your sign-up. Please try a different email or sign in." + : raw redirect(`/signup?error=${encodeURIComponent(msg)}`) } diff --git a/app/actions/esign.ts b/app/actions/esign.ts index 3fc213d..3e73279 100644 --- a/app/actions/esign.ts +++ b/app/actions/esign.ts @@ -1,9 +1,27 @@ "use server" import { revalidatePath } from "next/cache" +import { and, eq } from "drizzle-orm" +import { db } from "@/lib/db" +import { leases } from "@/lib/db/schema" import { getSessionUser } from "@/lib/session" import { getAccountContext } from "@/lib/account" -import { sendLeaseForSignature, getAdapter, type ESignProvider } from "@/lib/esign" +import { keyBelongsToOwner } from "@/lib/storage" +import { + sendLeaseForSignature, + getAdapter, + saveEsignConnection, + disconnectEsign, + type ESignProvider, +} from "@/lib/esign" + +async function ownerGuard() { + const user = await getSessionUser() + if (!user) throw new Error("Unauthorized") + const ctx = await getAccountContext(user.id) + if (!ctx.isOwner) throw new Error("Only the account owner can manage integrations") + return ctx +} export async function sendLeaseForSignatureAction(leaseId: string, provider: string) { const user = await getSessionUser() @@ -15,3 +33,47 @@ export async function sendLeaseForSignatureAction(leaseId: string, provider: str revalidatePath(`/leases/${leaseId}`) return { ok: true } } + +/** Connect Dropbox Sign by validating and storing the landlord's API key. */ +export async function connectDropboxSign(apiKey: string) { + const ctx = await ownerGuard() + const adapter = getAdapter("dropbox_sign") + if (!adapter) throw new Error("Unknown provider") + const tokens = await adapter.connectApiKey(typeof apiKey === "string" ? apiKey : "") + await saveEsignConnection(ctx.ownerId, "dropbox_sign", tokens) + revalidatePath("/settings/integrations") + return { ok: true, accountName: tokens.accountName } +} + +export async function disconnectEsignAction(provider: string) { + const ctx = await ownerGuard() + if (!getAdapter(provider)) throw new Error("Unknown provider") + await disconnectEsign(ctx.ownerId, provider as ESignProvider) + revalidatePath("/settings/integrations") + return { ok: true } +} + +/** + * Attach an already-uploaded document (via /api/upload) to a lease. Validates + * the file belongs to the caller's namespace to prevent cross-tenant refs. + */ +export async function setLeaseDocument(leaseId: string, fileUrl: string) { + const user = await getSessionUser() + if (!user) throw new Error("Unauthorized") + const ctx = await getAccountContext(user.id) + if (!ctx.canWrite) throw new Error("You don't have permission to do that") + + const prefix = "/api/files/" + if (typeof fileUrl !== "string" || !fileUrl.startsWith(prefix)) throw new Error("Invalid document reference") + if (!keyBelongsToOwner(fileUrl.slice(prefix.length), ctx.ownerId)) throw new Error("Invalid document reference") + + const [row] = await db + .update(leases) + .set({ document_url: fileUrl }) + .where(and(eq(leases.id, leaseId), eq(leases.user_id, ctx.ownerId))) + .returning({ id: leases.id }) + if (!row) throw new Error("Lease not found") + + revalidatePath(`/leases/${leaseId}`) + return { ok: true, url: fileUrl } +} diff --git a/app/api/ai/ask/route.ts b/app/api/ai/ask/route.ts index 47c281f..7142633 100644 --- a/app/api/ai/ask/route.ts +++ b/app/api/ai/ask/route.ts @@ -13,7 +13,8 @@ import { } from "@/lib/db/schema" import { getSessionUser } from "@/lib/session" import { getEffectiveOwnerId } from "@/lib/account" -import { openai } from "@/lib/ai/client" +import { aiConfigured, AI_UNCONFIGURED_ERROR } from "@/lib/ai/client" +import { aiComplete } from "@/lib/ai/provider" import { enforceAiQuota } from "@/lib/ai/usage" import { dataBlock } from "@/lib/ai/prompts" @@ -21,6 +22,9 @@ export async function POST(request: Request) { const user = await getSessionUser() if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) + // Before the quota check so an unconfigured server never burns a call. + if (!aiConfigured()) return NextResponse.json({ error: AI_UNCONFIGURED_ERROR }, { status: 503 }) + const quota = await enforceAiQuota(user.id, "ai_ask") if (!quota.ok) return NextResponse.json({ error: quota.error }, { status: quota.status }) @@ -154,16 +158,22 @@ ${dataBlock("EXPIRING LEASES", JSON.stringify(expiringLeases, null, 2))} Answer the landlord's question in a helpful, concise, and professional manner. Use bullet points where appropriate. Be specific with numbers from the data above. If the question is unrelated to property management, politely redirect. ` - const completion = await openai.chat.completions.create({ - model: "gpt-4o-mini", - max_tokens: 1024, - messages: [ - { role: "system", content: context }, - { role: "user", content: question }, - ], - }) - - const answer = completion.choices[0].message.content ?? "" + let answer: string + try { + answer = await aiComplete({ + messages: [ + { role: "system", content: context }, + { role: "user", content: question }, + ], + maxTokens: 1024, + }) + } catch (err) { + console.error("[ai/ask] AI request failed:", err) + return NextResponse.json( + { error: "The AI service is temporarily unavailable. Please try again in a moment." }, + { status: 502 } + ) + } return NextResponse.json({ answer, usage: { used: quota.used, limit: quota.limit } }) } diff --git a/app/api/ai/maintenance-summary/route.ts b/app/api/ai/maintenance-summary/route.ts index 8d204fc..1391d6b 100644 --- a/app/api/ai/maintenance-summary/route.ts +++ b/app/api/ai/maintenance-summary/route.ts @@ -4,7 +4,8 @@ import { db } from "@/lib/db" import { properties, maintenance_requests } from "@/lib/db/schema" import { getSessionUser } from "@/lib/session" import { getEffectiveOwnerId } from "@/lib/account" -import { openai } from "@/lib/ai/client" +import { aiConfigured, AI_UNCONFIGURED_ERROR } from "@/lib/ai/client" +import { aiComplete } from "@/lib/ai/provider" import { MAINTENANCE_SUMMARY_PROMPT, dataBlock } from "@/lib/ai/prompts" import { enforceAiQuota } from "@/lib/ai/usage" @@ -12,6 +13,9 @@ export async function POST(request: Request) { const user = await getSessionUser() if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) + // Before the quota check so an unconfigured server never burns a call. + if (!aiConfigured()) return NextResponse.json({ error: AI_UNCONFIGURED_ERROR }, { status: 503 }) + const quota = await enforceAiQuota(user.id, "ai_maintenance_summary") if (!quota.ok) return NextResponse.json({ error: quota.error }, { status: quota.status }) @@ -39,9 +43,7 @@ export async function POST(request: Request) { columns: { name: true }, }) - const completion = await openai.chat.completions.create({ - model: "gpt-4o-mini", - max_tokens: 1024, + const text = await aiComplete({ messages: [ { role: "system", content: MAINTENANCE_SUMMARY_PROMPT }, { @@ -49,13 +51,13 @@ export async function POST(request: Request) { content: `${dataBlock("PROPERTY NAME", property?.name ?? "Unknown")}\n\n${dataBlock("MAINTENANCE REQUESTS", JSON.stringify(requests, null, 2))}`, }, ], + maxTokens: 1024, + json: true, }) - const text = completion.choices[0].message.content ?? "" - let summary try { - summary = JSON.parse(text.replace(/```json\n?/g, "").replace(/```\n?/g, "").trim()) + summary = JSON.parse(text) } catch { return NextResponse.json({ error: "Failed to parse AI response" }, { status: 500 }) } diff --git a/app/api/ai/predictions/route.ts b/app/api/ai/predictions/route.ts index c6f63ba..332aaa5 100644 --- a/app/api/ai/predictions/route.ts +++ b/app/api/ai/predictions/route.ts @@ -13,7 +13,8 @@ import { } from "@/lib/db/schema" import { getSessionUser } from "@/lib/session" import { getEffectiveOwnerId, getAccountContext } from "@/lib/account" -import { openai } from "@/lib/ai/client" +import { aiConfigured, AI_UNCONFIGURED_ERROR } from "@/lib/ai/client" +import { aiComplete } from "@/lib/ai/provider" import { logActivity } from "@/lib/activity" import { enforceAiQuota } from "@/lib/ai/usage" import { dataBlock } from "@/lib/ai/prompts" @@ -38,6 +39,9 @@ export async function POST() { const user = await getSessionUser() if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) + // Before the quota check so an unconfigured server never burns a call. + if (!aiConfigured()) return NextResponse.json({ error: AI_UNCONFIGURED_ERROR }, { status: 503 }) + const quota = await enforceAiQuota(user.id, "ai_predictions") if (!quota.ok) return NextResponse.json({ error: quota.error }, { status: quota.status }) @@ -174,16 +178,15 @@ Generate a JSON object with key "predictions" containing an array of 5-7 predict Only return valid JSON, no other text.` - const completion = await openai.chat.completions.create({ - model: "gpt-4o-mini", - max_tokens: 2000, + const content = await aiComplete({ messages: [{ role: "user", content: prompt }], - response_format: { type: "json_object" }, + maxTokens: 2000, + json: true, }) let predictions: any[] = [] try { - const parsed = JSON.parse(completion.choices[0].message.content ?? "{}") + const parsed = JSON.parse(content || "{}") predictions = Array.isArray(parsed) ? parsed : (parsed.predictions ?? []) } catch { return NextResponse.json({ error: "Failed to parse AI response" }, { status: 500 }) diff --git a/app/api/ai/recommendations/route.ts b/app/api/ai/recommendations/route.ts index 8fa6fab..ddb8e50 100644 --- a/app/api/ai/recommendations/route.ts +++ b/app/api/ai/recommendations/route.ts @@ -13,7 +13,8 @@ import { } from "@/lib/db/schema" import { getSessionUser } from "@/lib/session" import { getEffectiveOwnerId, getAccountContext } from "@/lib/account" -import { openai } from "@/lib/ai/client" +import { aiConfigured, AI_UNCONFIGURED_ERROR } from "@/lib/ai/client" +import { aiComplete } from "@/lib/ai/provider" import { logActivity } from "@/lib/activity" import { enforceAiQuota } from "@/lib/ai/usage" import { dataBlock } from "@/lib/ai/prompts" @@ -37,6 +38,9 @@ export async function POST() { const user = await getSessionUser() if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) + // Before the quota check so an unconfigured server never burns a call. + if (!aiConfigured()) return NextResponse.json({ error: AI_UNCONFIGURED_ERROR }, { status: 503 }) + const quota = await enforceAiQuota(user.id, "ai_recommendations") if (!quota.ok) return NextResponse.json({ error: quota.error }, { status: quota.status }) @@ -167,13 +171,12 @@ Only return valid JSON, no other text.` let recommendations: any[] = [] try { - const completion = await openai.chat.completions.create({ - model: "gpt-4o-mini", - max_tokens: 1500, + const content = await aiComplete({ messages: [{ role: "user", content: prompt }], - response_format: { type: "json_object" }, + maxTokens: 1500, + json: true, }) - const parsed = JSON.parse(completion.choices[0].message.content ?? "{}") + const parsed = JSON.parse(content || "{}") recommendations = Array.isArray(parsed) ? parsed : (parsed.recommendations ?? []) } catch (err: any) { return NextResponse.json({ error: err?.message ?? "AI generation failed" }, { status: 500 }) diff --git a/app/api/ai/rent-receipt/route.ts b/app/api/ai/rent-receipt/route.ts index 77df46b..4054a0d 100644 --- a/app/api/ai/rent-receipt/route.ts +++ b/app/api/ai/rent-receipt/route.ts @@ -1,7 +1,8 @@ import { NextResponse } from "next/server" import { z } from "zod" import { getSessionUser } from "@/lib/session" -import { openai } from "@/lib/ai/client" +import { aiConfigured, AI_UNCONFIGURED_ERROR } from "@/lib/ai/client" +import { aiComplete } from "@/lib/ai/provider" import { RENT_RECEIPT_PROMPT, dataBlock } from "@/lib/ai/prompts" import { enforceAiQuota } from "@/lib/ai/usage" @@ -25,6 +26,9 @@ export async function POST(request: Request) { const user = await getSessionUser() if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) + // Before the quota check so an unconfigured server never burns a call. + if (!aiConfigured()) return NextResponse.json({ error: AI_UNCONFIGURED_ERROR }, { status: 503 }) + const quota = await enforceAiQuota(user.id, "ai_rent_receipt") if (!quota.ok) return NextResponse.json({ error: quota.error }, { status: quota.status }) @@ -36,9 +40,7 @@ export async function POST(request: Request) { // Pass only the whitelisted, validated fields to the model. const { payment_id, ...receiptFields } = parsed.data - const completion = await openai.chat.completions.create({ - model: "gpt-4o-mini", - max_tokens: 1024, + const text = await aiComplete({ messages: [ { role: "system", content: RENT_RECEIPT_PROMPT }, { @@ -46,13 +48,13 @@ export async function POST(request: Request) { content: dataBlock("PAYMENT DETAILS", JSON.stringify(receiptFields, null, 2)), }, ], + maxTokens: 1024, + json: true, }) - const text = completion.choices[0].message.content ?? "" - let receipt try { - receipt = JSON.parse(text.replace(/```json\n?/g, "").replace(/```\n?/g, "").trim()) + receipt = JSON.parse(text) } catch { return NextResponse.json({ error: "Failed to parse AI response" }, { status: 500 }) } diff --git a/app/api/documents/[id]/route.ts b/app/api/documents/[id]/route.ts index 590cc3c..eb67e90 100644 --- a/app/api/documents/[id]/route.ts +++ b/app/api/documents/[id]/route.ts @@ -44,7 +44,7 @@ export async function DELETE(_: Request, { params }: { params: Promise<{ id: str await db.delete(documents).where(and(eq(documents.id, id), eq(documents.user_id, ownerId))) if (doc.storage_path) { - await deleteFile(doc.storage_path) + await deleteFile(doc.storage_path, ownerId) } return NextResponse.json({ success: true }) diff --git a/app/api/documents/route.ts b/app/api/documents/route.ts index d3b12bb..9811730 100644 --- a/app/api/documents/route.ts +++ b/app/api/documents/route.ts @@ -3,7 +3,14 @@ import { and, desc, eq } from "drizzle-orm" import { db } from "@/lib/db" import { documents, properties } from "@/lib/db/schema" import { getSessionUser } from "@/lib/session" -import { saveFile, isAllowedUploadExt, StorageNotConfiguredError } from "@/lib/storage" +import { + saveFile, + isAllowedUploadExt, + StorageNotConfiguredError, + keyBelongsToOwner, + contentMatchesExtension, + extOf, +} from "@/lib/storage" import { checkStorageLimit } from "@/lib/plan-limits" import { ownsProperty, ownsTenant } from "@/lib/db/ownership" import { getEffectiveOwnerId, getAccountContext } from "@/lib/account" @@ -57,6 +64,10 @@ export async function POST(request: Request) { if (!file) return NextResponse.json({ error: "No file provided" }, { status: 400 }) if (file.size > 20 * 1024 * 1024) return NextResponse.json({ error: "File too large (max 20 MB)" }, { status: 400 }) if (!isAllowedUploadExt(file.name)) return NextResponse.json({ error: "File type not allowed" }, { status: 400 }) + const head = Buffer.from(await file.slice(0, 16).arrayBuffer()) + if (!contentMatchesExtension(head, extOf(file.name))) { + return NextResponse.json({ error: "File content does not match its type" }, { status: 400 }) + } const storageError = await checkStorageLimit(ownerId, file.size) if (storageError) return NextResponse.json({ error: storageError }, { status: 403 }) @@ -113,6 +124,20 @@ export async function POST(request: Request) { return NextResponse.json({ error: "Tenant not found" }, { status: 404 }) } + // The file reference is client-supplied. Require it to be an /api/files URL + // inside the caller's OWN namespace, and derive storage_path from it — never + // trust a separate client storage_path (which could point at another tenant's + // object and later be deleted). Also blocks javascript:/external file_url values. + const FILES_PREFIX = "/api/files/" + const fileUrl = typeof body.file_url === "string" ? body.file_url : "" + if (!fileUrl.startsWith(FILES_PREFIX)) { + return NextResponse.json({ error: "file_url must reference an uploaded file" }, { status: 400 }) + } + const storagePath = fileUrl.slice(FILES_PREFIX.length) + if (!keyBelongsToOwner(storagePath, ownerId)) { + return NextResponse.json({ error: "Invalid file reference" }, { status: 403 }) + } + // Whitelist insertable columns — never trust client-supplied user_id/id/created_at. const [data] = await db .insert(documents) @@ -122,8 +147,8 @@ export async function POST(request: Request) { tenant_id: tenantId, name: body.name as string, category: (body.category as typeof documents.$inferInsert.category) ?? "other", - file_url: body.file_url as string, - storage_path: body.storage_path as string | undefined, + file_url: fileUrl, + storage_path: storagePath, file_type: body.file_type as string | undefined, file_size: body.file_size as number | undefined, }) diff --git a/app/api/esign/[provider]/callback/route.ts b/app/api/esign/[provider]/callback/route.ts new file mode 100644 index 0000000..229319d --- /dev/null +++ b/app/api/esign/[provider]/callback/route.ts @@ -0,0 +1,48 @@ +import { NextResponse } from "next/server" +import { cookies } from "next/headers" +import { getSessionUser } from "@/lib/session" +import { getAccountContext } from "@/lib/account" +import { getAdapter, saveEsignConnection, type ESignProvider } from "@/lib/esign" +import { verifyState, ESIGN_NONCE_COOKIE } from "@/lib/esign/state" + +// OAuth callback — exchanges the code for tokens and stores the connection. +export async function GET(request: Request, { params }: { params: Promise<{ provider: string }> }) { + const { provider } = await params + const adapter = getAdapter(provider) + const settings = new URL("/settings/integrations", request.url) + + const cookieStore = await cookies() + const nonceCookie = cookieStore.get(ESIGN_NONCE_COOKIE)?.value + const done = (p: Record) => { + for (const [k, v] of Object.entries(p)) settings.searchParams.set(k, v) + const res = NextResponse.redirect(settings) + res.cookies.set(ESIGN_NONCE_COOKIE, "", { path: "/", maxAge: 0 }) + return res + } + + const url = new URL(request.url) + const code = url.searchParams.get("code") + const state = url.searchParams.get("state") + const oauthError = url.searchParams.get("error") + + if (oauthError || !adapter || adapter.kind !== "oauth") return done({ error: "connect_failed" }) + + const st = state ? verifyState(state) : null + // CSRF: state nonce must match the cookie, and the session must be the same owner. + if (!code || !st || st.provider !== provider || !nonceCookie || nonceCookie !== st.nonce) { + return done({ error: "invalid_state" }) + } + const user = await getSessionUser() + if (!user) return done({ error: "invalid_state" }) + const ctx = await getAccountContext(user.id) + if (ctx.ownerId !== st.ownerId) return done({ error: "invalid_state" }) + + try { + const tokens = await adapter.exchangeCode(code) + if (!tokens.accountId || !tokens.baseUri) throw new Error("No account returned from provider") + await saveEsignConnection(st.ownerId, provider as ESignProvider, tokens) + return done({ connected: provider }) + } catch { + return done({ error: "connect_failed" }) + } +} diff --git a/app/api/esign/[provider]/connect/route.ts b/app/api/esign/[provider]/connect/route.ts new file mode 100644 index 0000000..0c357c1 --- /dev/null +++ b/app/api/esign/[provider]/connect/route.ts @@ -0,0 +1,49 @@ +import crypto from "crypto" +import { NextResponse } from "next/server" +import { getSessionUser } from "@/lib/session" +import { getAccountContext } from "@/lib/account" +import { getAdapter } from "@/lib/esign" +import { signState, ESIGN_NONCE_COOKIE } from "@/lib/esign/state" + +// Starts the OAuth connect flow for an e-signature provider (owner-only). +// API-key providers (Dropbox Sign) don't use this — they connect via a form. +export async function GET(request: Request, { params }: { params: Promise<{ provider: string }> }) { + const { provider } = await params + const adapter = getAdapter(provider) + const settings = new URL("/settings/integrations", request.url) + + if (!adapter) { + settings.searchParams.set("error", "unknown_provider") + return NextResponse.redirect(settings) + } + + const user = await getSessionUser() + if (!user) return NextResponse.redirect(new URL("/login", request.url)) + + const ctx = await getAccountContext(user.id) + if (!ctx.isOwner) { + settings.searchParams.set("error", "owner_only") + return NextResponse.redirect(settings) + } + if (adapter.kind !== "oauth") { + settings.searchParams.set("error", "use_api_key") + return NextResponse.redirect(settings) + } + if (!adapter.available()) { + settings.searchParams.set("error", "not_configured") + return NextResponse.redirect(settings) + } + + // Bind the round-trip to this browser: nonce in the signed state AND a cookie. + const nonce = crypto.randomUUID() + const state = signState({ ownerId: ctx.ownerId, provider, nonce }) + const res = NextResponse.redirect(adapter.getAuthUrl(state)) + res.cookies.set(ESIGN_NONCE_COOKIE, nonce, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + path: "/", + maxAge: 600, + }) + return res +} diff --git a/app/api/follow-ups/run/route.ts b/app/api/follow-ups/run/route.ts index b4f2de7..89add3c 100644 --- a/app/api/follow-ups/run/route.ts +++ b/app/api/follow-ups/run/route.ts @@ -1,15 +1,17 @@ import { NextResponse } from "next/server" import { getSessionUser } from "@/lib/session" -import { getEffectiveOwnerId } from "@/lib/account" +import { getAccountContext } from "@/lib/account" import { runFollowUpsForUser } from "@/lib/follow-ups" export async function POST() { const user = await getSessionUser() if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) - const ownerId = await getEffectiveOwnerId(user.id) + // Sends real outbound follow-ups — a mutating action, so viewers are blocked. + const ctx = await getAccountContext(user.id) + if (!ctx.canWrite) return NextResponse.json({ error: "Forbidden" }, { status: 403 }) - const result = await runFollowUpsForUser(ownerId) + const result = await runFollowUpsForUser(ctx.ownerId) // Preserve the original response shape ({ sent, results }). The detailed // per-follow-up rows now live only in follow_up_log; the client re-fetches diff --git a/app/api/integrations/[provider]/callback/route.ts b/app/api/integrations/[provider]/callback/route.ts index 313f285..6346712 100644 --- a/app/api/integrations/[provider]/callback/route.ts +++ b/app/api/integrations/[provider]/callback/route.ts @@ -1,37 +1,51 @@ import { NextResponse } from "next/server" +import { cookies } from "next/headers" +import { getSessionUser } from "@/lib/session" +import { getAccountContext } from "@/lib/account" import { getProvider, saveConnection, type Provider } from "@/lib/accounting" -import { verifyState } from "@/lib/accounting/state" +import { verifyState, OAUTH_NONCE_COOKIE } from "@/lib/accounting/state" // OAuth callback — exchanges the code for tokens and stores the connection. export async function GET(request: Request, { params }: { params: Promise<{ provider: string }> }) { const { provider: pid } = await params const prov = getProvider(pid) - const url = new URL(request.url) const settings = new URL("/settings/integrations", request.url) + // Always clear the one-shot nonce cookie on the way out. + const cookieStore = await cookies() + const nonceCookie = cookieStore.get(OAUTH_NONCE_COOKIE)?.value + const done = (params: Record) => { + for (const [k, v] of Object.entries(params)) settings.searchParams.set(k, v) + const res = NextResponse.redirect(settings) + res.cookies.set(OAUTH_NONCE_COOKIE, "", { path: "/", maxAge: 0 }) + return res + } + + const url = new URL(request.url) const code = url.searchParams.get("code") const state = url.searchParams.get("state") const realmId = url.searchParams.get("realmId") // QuickBooks includes this const oauthError = url.searchParams.get("error") - if (oauthError || !prov) { - settings.searchParams.set("error", "connect_failed") - return NextResponse.redirect(settings) - } + if (oauthError || !prov) return done({ error: "connect_failed" }) const st = state ? verifyState(state) : null - if (!code || !st || st.provider !== pid) { - settings.searchParams.set("error", "invalid_state") - return NextResponse.redirect(settings) + // CSRF: the state's nonce must match the cookie set at connect time, and the + // current session must be the same owner that initiated the connect. + if (!code || !st || st.provider !== pid || !nonceCookie || nonceCookie !== st.nonce) { + return done({ error: "invalid_state" }) } + const user = await getSessionUser() + if (!user) return done({ error: "invalid_state" }) + const ctx = await getAccountContext(user.id) + if (ctx.ownerId !== st.ownerId) return done({ error: "invalid_state" }) try { const tokens = await prov.exchangeCode(code, realmId) if (!tokens.realmId) throw new Error("No organisation returned from provider") await saveConnection(st.ownerId, pid as Provider, tokens) - settings.searchParams.set("connected", pid) + return done({ connected: pid }) } catch { - settings.searchParams.set("error", "connect_failed") + return done({ error: "connect_failed" }) } - return NextResponse.redirect(settings) } diff --git a/app/api/integrations/[provider]/connect/route.ts b/app/api/integrations/[provider]/connect/route.ts index 743882c..fffa168 100644 --- a/app/api/integrations/[provider]/connect/route.ts +++ b/app/api/integrations/[provider]/connect/route.ts @@ -1,8 +1,9 @@ +import crypto from "crypto" import { NextResponse } from "next/server" import { getSessionUser } from "@/lib/session" import { getAccountContext } from "@/lib/account" import { getProvider } from "@/lib/accounting" -import { signState } from "@/lib/accounting/state" +import { signState, OAUTH_NONCE_COOKIE } from "@/lib/accounting/state" // Starts the OAuth connect flow for an accounting provider (owner-only). export async function GET(request: Request, { params }: { params: Promise<{ provider: string }> }) { @@ -28,6 +29,17 @@ export async function GET(request: Request, { params }: { params: Promise<{ prov return NextResponse.redirect(settings) } - const state = signState({ ownerId: ctx.ownerId, provider: pid }) - return NextResponse.redirect(prov.getAuthUrl(state)) + // Bind the OAuth round-trip to this browser: a random nonce goes into the + // signed state AND an httpOnly cookie; the callback requires them to match. + const nonce = crypto.randomUUID() + const state = signState({ ownerId: ctx.ownerId, provider: pid, nonce }) + const res = NextResponse.redirect(prov.getAuthUrl(state)) + res.cookies.set(OAUTH_NONCE_COOKIE, nonce, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + path: "/", + maxAge: 600, + }) + return res } diff --git a/app/api/paypal/cancel/route.ts b/app/api/paypal/cancel/route.ts deleted file mode 100644 index 92b91f2..0000000 --- a/app/api/paypal/cancel/route.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { NextResponse } from "next/server" -import { eq } from "drizzle-orm" -import { db } from "@/lib/db" -import { profiles } from "@/lib/db/schema" -import { getSessionUser } from "@/lib/session" -import { cancelSubscription } from "@/lib/paypal/checkout" - -// Cancel the signed-in user's PayPal subscription. The account keeps access -// until the paid period ends; the BILLING.SUBSCRIPTION.CANCELLED webhook does -// the final downgrade to starter. -export async function POST() { - const user = await getSessionUser() - if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) - - const profile = await db.query.profiles.findFirst({ - where: eq(profiles.id, user.id), - columns: { paypal_subscription_id: true }, - }) - if (!profile?.paypal_subscription_id) { - return NextResponse.json({ error: "No PayPal subscription to cancel" }, { status: 400 }) - } - - const ok = await cancelSubscription(profile.paypal_subscription_id) - if (!ok) return NextResponse.json({ error: "PayPal cancellation failed" }, { status: 502 }) - - await db - .update(profiles) - .set({ subscription_status: "canceled" }) - .where(eq(profiles.id, user.id)) - - return NextResponse.json({ ok: true }) -} diff --git a/app/api/paypal/checkout/route.ts b/app/api/paypal/checkout/route.ts deleted file mode 100644 index 2c6117c..0000000 --- a/app/api/paypal/checkout/route.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { NextResponse } from "next/server" -import { eq } from "drizzle-orm" -import { db } from "@/lib/db" -import { profiles } from "@/lib/db/schema" -import { getSessionUser } from "@/lib/session" -import { paypalConfigured } from "@/lib/paypal/client" -import { getPaypalPlanId } from "@/lib/paypal/plans" -import { createSubscription, createOrder } from "@/lib/paypal/checkout" -import { PLAN_AMOUNTS } from "@/lib/stripe/plans" - -const RECURRING = new Set(["pro", "landlord"]) - -// Start a PayPal checkout for a plan upgrade and return the approval URL. -// Recurring plans → Subscriptions API; lifetime → one-time Orders API. -export async function POST(request: Request) { - if (!paypalConfigured()) { - return NextResponse.json({ error: "PayPal is not configured" }, { status: 400 }) - } - const user = await getSessionUser() - if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) - - const { plan, interval } = (await request.json().catch(() => ({}))) as { - plan?: string - interval?: "month" | "year" - } - if (!plan || (plan !== "lifetime" && !RECURRING.has(plan))) { - return NextResponse.json({ error: "Invalid plan" }, { status: 400 }) - } - - const appUrl = process.env.NEXT_PUBLIC_APP_URL! - const cancelUrl = `${appUrl}/settings/billing?canceled=true` - - try { - if (plan === "lifetime") { - const { approveUrl } = await createOrder({ - amount: PLAN_AMOUNTS.lifetime, - userId: user.id, - plan: "lifetime", - returnUrl: `${appUrl}/api/paypal/return?type=order`, - cancelUrl, - }) - if (!approveUrl) throw new Error("PayPal did not return an approval URL") - return NextResponse.json({ url: approveUrl }) - } - - const billingInterval = interval === "year" ? "year" : "month" - const planId = getPaypalPlanId(plan as "pro" | "landlord", billingInterval) - if (!planId) { - return NextResponse.json({ error: "That plan isn't available on PayPal yet." }, { status: 400 }) - } - - const profile = await db.query.profiles.findFirst({ - where: eq(profiles.id, user.id), - columns: { email: true }, - }) - - const { approveUrl } = await createSubscription({ - planId, - userId: user.id, - plan, - email: profile?.email ?? user.email, - returnUrl: `${appUrl}/api/paypal/return?type=subscription`, - cancelUrl, - }) - if (!approveUrl) throw new Error("PayPal did not return an approval URL") - return NextResponse.json({ url: approveUrl }) - } catch (e) { - return NextResponse.json( - { error: (e as Error).message || "PayPal checkout failed" }, - { status: 502 } - ) - } -} diff --git a/app/api/paypal/return/route.ts b/app/api/paypal/return/route.ts deleted file mode 100644 index 689f45d..0000000 --- a/app/api/paypal/return/route.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { NextResponse } from "next/server" -import { getSessionUser } from "@/lib/session" -import { captureOrder, getSubscription, decodeCustomId } from "@/lib/paypal/checkout" -import { fulfillSubscription, fulfillLifetime } from "@/lib/paypal/fulfill" - -// PayPal redirects the approver back here. We finalize synchronously (capture -// the order / confirm the subscription) so the plan is live the moment they -// land on the billing page — the webhook is a backstop, not the only path. -export async function GET(request: Request) { - const url = new URL(request.url) - const type = url.searchParams.get("type") - const appUrl = process.env.NEXT_PUBLIC_APP_URL! - const ok = NextResponse.redirect(`${appUrl}/settings/billing?success=true`) - const fail = NextResponse.redirect(`${appUrl}/settings/billing?error=paypal`) - - const user = await getSessionUser() - if (!user) return NextResponse.redirect(`${appUrl}/login`) - - try { - if (type === "order") { - const orderId = url.searchParams.get("token") - if (!orderId) return fail - const captured = await captureOrder(orderId) - if (!captured || captured.status !== "COMPLETED") return fail - const decoded = decodeCustomId(captured.custom_id) - if (!decoded || decoded.userId !== user.id) return fail - await fulfillLifetime(user.id) - return ok - } - - // Subscription approval. - const subId = url.searchParams.get("subscription_id") - if (!subId) return fail - const sub = await getSubscription(subId) - if (!sub) return fail - const decoded = decodeCustomId(sub.custom_id) - // Only accept a subscription whose custom_id matches the signed-in user. - if (!decoded || decoded.userId !== user.id) return fail - - const active = sub.status === "ACTIVE" || sub.status === "APPROVED" - await fulfillSubscription( - user.id, - decoded.plan, - sub.id, - sub.billing_info?.next_billing_time, - active ? "active" : sub.status.toLowerCase() - ) - return ok - } catch { - return fail - } -} diff --git a/app/api/paypal/webhook/route.ts b/app/api/paypal/webhook/route.ts deleted file mode 100644 index 2a1b4cd..0000000 --- a/app/api/paypal/webhook/route.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { NextResponse } from "next/server" -import { verifyPaypalWebhook } from "@/lib/paypal/webhook" -import { decodeCustomId, getSubscription } from "@/lib/paypal/checkout" -import { fulfillSubscription, fulfillLifetime, markPaypalSubscriptionInactive } from "@/lib/paypal/fulfill" - -// Inbound PayPal webhook. Signature is verified via PayPal's API using -// PAYPAL_WEBHOOK_ID; unverified events are rejected. -export async function POST(request: Request) { - const body = await request.text() - - const valid = await verifyPaypalWebhook(request.headers, body) - if (!valid) return NextResponse.json({ error: "invalid signature" }, { status: 400 }) - - let event: { event_type?: string; resource?: Record } - try { - event = JSON.parse(body) - } catch { - return NextResponse.json({ ok: true }) - } - - const type = event.event_type ?? "" - const resource = (event.resource ?? {}) as Record - - try { - switch (type) { - case "BILLING.SUBSCRIPTION.ACTIVATED": - case "BILLING.SUBSCRIPTION.UPDATED": { - const decoded = decodeCustomId(resource.custom_id) - if (decoded && resource.id) { - await fulfillSubscription( - decoded.userId, - decoded.plan, - resource.id, - resource.billing_info?.next_billing_time, - "active" - ) - } - break - } - - case "PAYMENT.SALE.COMPLETED": { - // A recurring payment cleared — refresh status + next billing date. - const subId = resource.billing_agreement_id as string | undefined - if (subId) { - const sub = await getSubscription(subId) - const decoded = decodeCustomId(sub?.custom_id) - if (sub && decoded) { - await fulfillSubscription( - decoded.userId, - decoded.plan, - subId, - sub.billing_info?.next_billing_time, - "active" - ) - } - } - break - } - - case "BILLING.SUBSCRIPTION.CANCELLED": - case "BILLING.SUBSCRIPTION.EXPIRED": { - if (resource.id) { - await markPaypalSubscriptionInactive( - resource.id, - type.endsWith("CANCELLED") ? "canceled" : "expired", - true - ) - } - break - } - - case "BILLING.SUBSCRIPTION.SUSPENDED": { - if (resource.id) await markPaypalSubscriptionInactive(resource.id, "suspended", false) - break - } - - case "PAYMENT.CAPTURE.COMPLETED": { - // Lifetime order capture (backup to the return handler). - const decoded = decodeCustomId(resource.custom_id) - if (decoded && decoded.plan === "lifetime") await fulfillLifetime(decoded.userId) - break - } - } - } catch { - // Never loop forever on a handler bug — PayPal retries non-2xx. - } - - return NextResponse.json({ received: true }) -} diff --git a/app/api/profile/route.ts b/app/api/profile/route.ts index f4f75f6..5f7f50f 100644 --- a/app/api/profile/route.ts +++ b/app/api/profile/route.ts @@ -8,8 +8,17 @@ export async function GET() { const user = await getSessionUser() if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) + // Exclude bearer/secret + billing-id columns from the client payload. The + // calendar feed token and Stripe/PayPal ids are used server-side only. const profile = await db.query.profiles.findFirst({ where: eq(profiles.id, user.id), + columns: { + calendar_token: false, + stripe_customer_id: false, + stripe_subscription_id: false, + paypal_subscription_id: false, + billing_provider: false, + }, }) return NextResponse.json({ profile: profile ?? null }) diff --git a/app/api/upload/route.ts b/app/api/upload/route.ts index ae74d1a..8e5be3a 100644 --- a/app/api/upload/route.ts +++ b/app/api/upload/route.ts @@ -1,7 +1,13 @@ import { NextResponse } from "next/server" import { getSessionUser } from "@/lib/session" import { getAccountContext } from "@/lib/account" -import { saveFile, isAllowedUploadExt, StorageNotConfiguredError } from "@/lib/storage" +import { + saveFile, + isAllowedUploadExt, + StorageNotConfiguredError, + contentMatchesExtension, + extOf, +} from "@/lib/storage" import { checkStorageLimit } from "@/lib/plan-limits" const ALLOWED_SCOPES = ["property-images", "maintenance", "documents", "misc"] @@ -33,6 +39,12 @@ export async function POST(request: Request) { return NextResponse.json({ error: "File type not allowed" }, { status: 400 }) } + // Reject files whose real content doesn't match the claimed extension. + const head = Buffer.from(await file.slice(0, 16).arrayBuffer()) + if (!contentMatchesExtension(head, extOf(file.name))) { + return NextResponse.json({ error: "File content does not match its type" }, { status: 400 }) + } + // Enforce per-plan storage quota (accounts for everything already stored in // the owner's portfolio namespace). const storageError = await checkStorageLimit(ownerId, file.size) diff --git a/app/global-error.tsx b/app/global-error.tsx index 9e1ea83..d206570 100644 --- a/app/global-error.tsx +++ b/app/global-error.tsx @@ -1,5 +1,6 @@ "use client" +import * as Sentry from "@sentry/nextjs" import { useEffect } from "react" export default function GlobalError({ @@ -10,6 +11,7 @@ export default function GlobalError({ reset: () => void }) { useEffect(() => { + Sentry.captureException(error) console.error(error) }, [error]) diff --git a/app/manifest.ts b/app/manifest.ts index cb4d200..4bf20d8 100644 --- a/app/manifest.ts +++ b/app/manifest.ts @@ -12,11 +12,23 @@ export default function manifest(): MetadataRoute.Manifest { theme_color: "#09090b", icons: [ { - src: "/logo-mark.png", + src: "/icon-192.png", type: "image/png", - sizes: "100x100", + sizes: "192x192", purpose: "any", }, + { + src: "/icon-512.png", + type: "image/png", + sizes: "512x512", + purpose: "any", + }, + { + src: "/icon-maskable-512.png", + type: "image/png", + sizes: "512x512", + purpose: "maskable", + }, ], } } diff --git a/app/robots.ts b/app/robots.ts new file mode 100644 index 0000000..e9306a6 --- /dev/null +++ b/app/robots.ts @@ -0,0 +1,44 @@ +import type { MetadataRoute } from "next" + +export default function robots(): MetadataRoute.Robots { + const base = process.env.NEXT_PUBLIC_APP_URL ?? "https://propertymanagement.network" + + return { + rules: { + userAgent: "*", + allow: "/", + // Private/app areas. Mirrors PROTECTED_PATHS in proxy.ts, plus the API + // surface and the token-gated tenant portal (both private but enforced + // outside the cookie proxy). `/tenant-portal/` keeps the trailing slash so + // it doesn't also block the indexable `/tenant-portal-info` marketing page. + disallow: [ + "/dashboard", + "/admin", + "/api/", + "/settings", + "/onboarding", + "/team", + "/tenant-portal/", + "/calendar", + "/inspections", + "/vendors", + "/reports", + "/activity", + "/ai", + "/ai-dashboard", + "/predictions", + "/recommendations", + "/impact", + "/follow-ups", + "/properties", + "/tenants", + "/rent", + "/maintenance", + "/leases", + "/expenses", + ], + }, + sitemap: `${base}/sitemap.xml`, + host: base, + } +} diff --git a/app/robots.txt/route.ts b/app/robots.txt/route.ts deleted file mode 100644 index cc767ca..0000000 --- a/app/robots.txt/route.ts +++ /dev/null @@ -1,21 +0,0 @@ -export async function GET() { - const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? "https://propertymanagement.network" - const body = `User-agent: * -Allow: / -Disallow: /dashboard -Disallow: /properties -Disallow: /tenants -Disallow: /rent -Disallow: /maintenance -Disallow: /leases -Disallow: /expenses -Disallow: /settings -Disallow: /tenant-portal/ -Disallow: /api/ - -Sitemap: ${appUrl}/sitemap.xml` - - return new Response(body, { - headers: { "Content-Type": "text/plain" }, - }) -} diff --git a/app/sitemap.ts b/app/sitemap.ts index c150955..8cdd331 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -1,17 +1,38 @@ import type { MetadataRoute } from "next" +import { LEGAL_PAGES } from "@/lib/legal" + +const base = process.env.NEXT_PUBLIC_APP_URL ?? "https://propertymanagement.network" + +type ChangeFrequency = MetadataRoute.Sitemap[number]["changeFrequency"] + +// Single source of truth for the public, indexable URL surface. Every entry +// must resolve to a real 200 page that is NOT noindex'd. Private/app routes are +// blocked in app/robots.ts, and the /login and /forgot-password auth pages are +// noindex, so all three are intentionally omitted here. /signup is kept as a +// conversion landing page. +const PAGES: { path: string; changeFrequency: ChangeFrequency; priority: number }[] = [ + { path: "/", changeFrequency: "weekly", priority: 1 }, + { path: "/tenant-portal-info", changeFrequency: "monthly", priority: 0.8 }, + { path: "/api-docs", changeFrequency: "monthly", priority: 0.8 }, + { path: "/signup", changeFrequency: "monthly", priority: 0.7 }, + // Legal pages are derived from the shared LEGAL_PAGES constant (the same list + // the footer renders) so the sitemap can never drift from the real routes. + ...LEGAL_PAGES.map((page) => ({ + path: page.href, + changeFrequency: "yearly" as const, + priority: 0.3, + })), +] export default function sitemap(): MetadataRoute.Sitemap { - const base = process.env.NEXT_PUBLIC_APP_URL ?? "https://propertymanagement.network" - const lastModified = new Date("2026-07-01") + // Build-time timestamp, refreshed on every deploy. We don't track per-page + // modification dates, so a single honest "last built" date is used throughout. + const lastModified = new Date() - return [ - { url: base, lastModified, changeFrequency: "weekly", priority: 1 }, - { url: `${base}/tenant-portal-info`, lastModified, changeFrequency: "monthly", priority: 0.8 }, - { url: `${base}/api-docs`, lastModified, changeFrequency: "monthly", priority: 0.6 }, - { url: `${base}/signup`, lastModified, changeFrequency: "monthly", priority: 0.7 }, - { url: `${base}/privacy`, lastModified, changeFrequency: "yearly", priority: 0.3 }, - { url: `${base}/terms`, lastModified, changeFrequency: "yearly", priority: 0.3 }, - { url: `${base}/cookie-policy`, lastModified, changeFrequency: "yearly", priority: 0.3 }, - { url: `${base}/gdpr`, lastModified, changeFrequency: "yearly", priority: 0.3 }, - ] + return PAGES.map(({ path, changeFrequency, priority }) => ({ + url: path === "/" ? base : `${base}${path}`, + lastModified, + changeFrequency, + priority, + })) } diff --git a/components/admin/ai-provider-toggle.tsx b/components/admin/ai-provider-toggle.tsx new file mode 100644 index 0000000..4f954de --- /dev/null +++ b/components/admin/ai-provider-toggle.tsx @@ -0,0 +1,119 @@ +"use client" + +import { useState, useTransition } from "react" +import { toast } from "sonner" +import { Sparkles, Check, AlertTriangle } from "lucide-react" +import { setAiProviderAction } from "@/app/actions/admin" + +type Provider = "openai" | "anthropic" + +const LABELS: Record = { openai: "OpenAI", anthropic: "Anthropic (Claude)" } + +export function AiProviderToggle({ + selected, + effective, + openaiConfigured, + anthropicConfigured, + openaiModel, + anthropicModel, +}: { + selected: Provider + effective: Provider + openaiConfigured: boolean + anthropicConfigured: boolean + openaiModel: string + anthropicModel: string +}) { + const [current, setCurrent] = useState(selected) + const [pending, startTransition] = useTransition() + + const configured: Record = { openai: openaiConfigured, anthropic: anthropicConfigured } + const models: Record = { openai: openaiModel, anthropic: anthropicModel } + + function choose(next: Provider) { + if (next === current || pending) return + const prev = current + setCurrent(next) + startTransition(async () => { + try { + await setAiProviderAction(next) + toast.success(`AI provider set to ${LABELS[next]}`) + } catch { + setCurrent(prev) // revert optimistic change + toast.error("Couldn't switch the AI provider. Try again.") + } + }) + } + + // When the selected provider has no key on the server, AI falls back to the + // other configured provider (see lib/ai/provider). Surface that clearly. + const fallbackActive = effective !== current + const noneConfigured = !openaiConfigured && !anthropicConfigured + + const options: Provider[] = ["openai", "anthropic"] + + return ( +
+
+ +

AI provider

+
+ +
+

+ Choose which LLM powers all AI features (assistant, recommendations, predictions, summaries, + receipts). Applies to everyone immediately. +

+ +
+ {options.map((p) => { + const active = current === p + return ( + + ) + })} +
+ + {noneConfigured ? ( +

+ + No AI provider key is set on the server — AI features return a 503 until{" "} + OPENAI_API_KEY or ANTHROPIC_API_KEY is configured. +

+ ) : fallbackActive ? ( +

+ + {LABELS[current]} has no API key on this server, so AI is temporarily running on{" "} + {LABELS[effective]}. Add the key to use {LABELS[current]}. +

+ ) : null} +
+
+ ) +} diff --git a/components/dashboard/esign-integrations.tsx b/components/dashboard/esign-integrations.tsx new file mode 100644 index 0000000..e650225 --- /dev/null +++ b/components/dashboard/esign-integrations.tsx @@ -0,0 +1,268 @@ +"use client" + +import { useEffect, useState, useTransition } from "react" +import { toast } from "sonner" +import { + PenLine, + Link2, + CheckCircle2, + AlertTriangle, + KeyRound, + ChevronDown, + ExternalLink, + Loader2, +} from "lucide-react" +import { connectDropboxSign, disconnectEsignAction } from "@/app/actions/esign" + +type Adapter = { id: string; label: string; kind: "oauth" | "apikey"; available: boolean } +type Conn = { provider: string; accountName: string | null; status: string; lastError: string | null } + +const ERR_MSG: Record = { + connect_failed: "Connection failed — please try again.", + invalid_state: "The connection link expired or was invalid. Please retry.", + owner_only: "Only the account owner can manage integrations.", + not_configured: "E-signature isn't enabled on this server yet.", + unknown_provider: "Unknown provider.", + use_api_key: "That provider connects with an API key, not a redirect.", +} + +const LABEL: Record = { docusign: "DocuSign", dropbox_sign: "Dropbox Sign" } + +function StatusPill({ status }: { status: string }) { + const error = status === "error" + return ( + + {error ? : } + {error ? "Error" : "Connected"} + + ) +} + +export function EsignIntegrations({ + adapters, + connections, + isOwner, + flash, + webhookUrl, +}: { + adapters: Adapter[] + connections: Conn[] + isOwner: boolean + flash: { connected?: string; error?: string } + webhookUrl: string +}) { + const connByProvider: Record = Object.fromEntries(connections.map((c) => [c.provider, c])) + const [pending, start] = useTransition() + const [busy, setBusy] = useState(null) + const [open, setOpen] = useState(null) // which provider's instructions are expanded + const [apiKey, setApiKey] = useState("") + const [showKeyForm, setShowKeyForm] = useState(false) + + useEffect(() => { + if (flash.connected) toast.success(`Connected to ${LABEL[flash.connected] ?? flash.connected}`) + if (flash.error) toast.error(ERR_MSG[flash.error] ?? "Something went wrong") + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + function connectDbx() { + const key = apiKey.trim() + if (!key) return + setBusy("dropbox_sign") + start(async () => { + try { + const r = await connectDropboxSign(key) + toast.success(`Connected ${r.accountName ?? "Dropbox Sign"}`) + setApiKey("") + setShowKeyForm(false) + } catch (e) { + toast.error((e as Error).message || "Couldn't connect") + } finally { + setBusy(null) + } + }) + } + + function remove(id: string) { + if (!confirm(`Disconnect ${LABEL[id] ?? id}? You won't be able to send leases through it until you reconnect.`)) return + setBusy(id) + start(async () => { + try { + await disconnectEsignAction(id) + toast.success("Disconnected") + } catch { + toast.error("Couldn't disconnect") + } finally { + setBusy(null) + } + }) + } + + if (!isOwner) { + return ( +
+ Only the account owner can connect e-signature providers. +
+ ) + } + + return ( +
+ {/* Intro / how it works */} +
+
+ +

Send leases for e-signature

+
+

+ Connect your own DocuSign or Dropbox Sign account so signed leases carry + your brand and audit trail — and the signing costs stay on your provider plan, not ours. Once connected, a + “Send for signature” button appears on every lease that has a document + and a tenant email. +

+
    +
  1. 1. Connect your provider below (one-time).
  2. +
  3. 2. Open a lease → upload the lease PDF.
  4. +
  5. 3. Click “Send via DocuSign / Dropbox Sign.” The tenant signs; the status updates here automatically and the signed copy is saved back to the lease.
  6. +
+
+ + {adapters.map((a) => { + const conn = connByProvider[a.id] + const isBusy = pending && busy === a.id + const instructionsOpen = open === a.id + return ( +
+
+
+
+ +
+
+

{a.label}

+ {conn ? ( +

{conn.accountName ?? "Connected"}

+ ) : ( +

+ {a.kind === "oauth" ? "Connect with your DocuSign login" : "Connect with your API key"} +

+ )} +
+
+ {conn ? ( + + ) : !a.available ? ( + + Not available + + ) : null} +
+ + {conn?.status === "error" && conn.lastError && ( +

{conn.lastError}

+ )} + + {/* Actions */} +
+ {conn ? ( + + ) : !a.available ? ( +

+ Ask your administrator to enable {a.label} (server credentials aren't configured). +

+ ) : a.kind === "oauth" ? ( + + Connect {a.label} + + ) : showKeyForm ? ( +
+ setApiKey(e.target.value)} + placeholder="Paste your Dropbox Sign API key" + className="flex-1 rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-xs text-white placeholder-white/30 outline-none focus:border-indigo-500/50" + /> + +
+ ) : ( + + )} + + {a.available && ( + + )} +
+ + {/* Instructions */} + {instructionsOpen && ( +
+ {a.id === "docusign" ? ( +
    +
  1. + 1. You need an active{" "} + + DocuSign eSignature plan + . +
  2. +
  3. 2. Click Connect DocuSign above.
  4. +
  5. 3. Log in to your DocuSign account and click Allow to grant access.
  6. +
  7. 4. You'll return here connected — no webhook setup needed. Signed-status updates and the completed PDF flow back automatically.
  8. +
+ ) : ( +
    +
  1. + 1. In Dropbox Sign, open{" "} + + Settings → API + {" "} + and copy your API key. +
  2. +
  3. 2. Paste it above and click Connect.
  4. +
  5. + 3. In the same API settings, set your account callback URL to: + {webhookUrl} + This lets us receive signed-status updates. +
  6. +
+ )} +
+ )} +
+ ) + })} + +

+ Your credentials are encrypted at rest and never leave the server. We only send the leases you explicitly submit. +

+
+ ) +} diff --git a/components/forms/checkout-button.tsx b/components/forms/checkout-button.tsx index a86a389..abaa9ed 100644 --- a/components/forms/checkout-button.tsx +++ b/components/forms/checkout-button.tsx @@ -9,7 +9,6 @@ export function CheckoutButton({ highlight, interval = "month", annualAvailable = false, - paypalEnabled = false, }: { plan: string label: string @@ -18,11 +17,8 @@ export function CheckoutButton({ // When true, show a monthly/annual choice. Only pass this for subscription // plans and only when annual billing is actually configured server-side. annualAvailable?: boolean - // When true, also offer "Pay with PayPal" using the same interval choice. - paypalEnabled?: boolean }) { const [loading, setLoading] = useState(false) - const [paypalLoading, setPaypalLoading] = useState(false) const [chosenInterval, setChosenInterval] = useState<"month" | "year">(interval) const effectiveInterval = annualAvailable ? chosenInterval : interval @@ -39,21 +35,6 @@ export function CheckoutButton({ else setLoading(false) } - async function handlePaypal() { - setPaypalLoading(true) - const res = await fetch("/api/paypal/checkout", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ plan, interval: effectiveInterval }), - }) - const data = await res.json() - if (data.url) window.location.href = data.url - else { - setPaypalLoading(false) - if (data.error) alert(data.error) - } - } - return (
{annualAvailable && ( @@ -82,7 +63,7 @@ export function CheckoutButton({ )} - - {paypalEnabled && ( - - )}
) } diff --git a/components/forms/esign-lease.tsx b/components/forms/esign-lease.tsx index 3e2c3e6..ae782e8 100644 --- a/components/forms/esign-lease.tsx +++ b/components/forms/esign-lease.tsx @@ -1,8 +1,9 @@ "use client" import { useTransition } from "react" +import Link from "next/link" import { toast } from "sonner" -import { PenLine, CheckCircle2, Clock, XCircle, AlertTriangle } from "lucide-react" +import { PenLine, CheckCircle2, Clock, XCircle, AlertTriangle, Download } from "lucide-react" import { formatDate } from "@/lib/utils" import { sendLeaseForSignatureAction } from "@/app/actions/esign" @@ -12,11 +13,12 @@ type Req = { status: string signer_email: string document_name: string | null + signed_document_url: string | null sent_at: string | null completed_at: string | null last_error: string | null } -type Prov = { id: string; label: string; configured: boolean } +type Prov = { id: string; label: string } const STATUS: Record = { sent: { label: "Awaiting signature", cls: "text-amber-400 bg-amber-500/10 border-amber-500/20", Icon: Clock }, @@ -29,18 +31,17 @@ const PROVIDER_LABEL: Record = { docusign: "DocuSign", dropbox_s export function EsignLease({ leaseId, - providers, + connected, requests, canSend, disabledReason, }: { leaseId: string - providers: Prov[] + connected: Prov[] requests: Req[] canSend: boolean disabledReason: string }) { - const configured = providers.filter((p) => p.configured) const [pending, start] = useTransition() function send(provider: string) { @@ -74,6 +75,16 @@ export function EsignLease({ {r.completed_at ? ` · Signed ${formatDate(r.completed_at)}` : ""} {r.status === "error" && r.last_error ? ` · ${r.last_error}` : ""}

+ {r.signed_document_url && ( + + Signed document + + )}
{s.label} @@ -84,11 +95,16 @@ export function EsignLease({ )} - {configured.length === 0 ? ( -

Configure DocuSign or Dropbox Sign on the server to send leases for e-signature.

+ {connected.length === 0 ? ( +

+ + Connect DocuSign or Dropbox Sign + {" "} + in Settings → Integrations to send leases for signature. +

) : canSend ? (
- {configured.map((p) => ( + {connected.map((p) => ( - ) -} diff --git a/components/marketing/structured-data.tsx b/components/marketing/structured-data.tsx index 50c229e..2f03f7f 100644 --- a/components/marketing/structured-data.tsx +++ b/components/marketing/structured-data.tsx @@ -1,5 +1,19 @@ +import { PLAN_AMOUNTS, getPlanLabel } from "@/lib/stripe/plans" +import type { Plan } from "@/types" + const base = process.env.NEXT_PUBLIC_APP_URL ?? "https://propertymanagement.network" +// Real plans + displayed prices from lib/stripe/plans.ts (single source of truth). +// Annual billing is auto-provisioned at checkout and has no fixed amount here, so +// we advertise only the monthly / one-time base prices that actually exist. +const planOrder: Plan[] = ["starter", "pro", "landlord", "lifetime"] +const planOffers = planOrder.map((plan) => ({ + "@type": "Offer", + name: getPlanLabel(plan), + price: String(PLAN_AMOUNTS[plan]), + priceCurrency: "USD", +})) + // Mirrors the visible FAQ content in components/marketing/faq.tsx. // Keep these in sync with that source so the JSON-LD matches what users see. const faqs = [ @@ -43,6 +57,11 @@ const organization: Record = { name: "Property Management Network", url: base, logo: `${base}/logo-mark.png`, + contactPoint: { + "@type": "ContactPoint", + contactType: "customer support", + email: "support@propertymanagement.network", + }, sameAs: [ "https://twitter.com/propertymgmtnet", "https://github.com/propertymanagement-network", @@ -64,11 +83,7 @@ const softwareApplication: Record = { operatingSystem: "Web", description: "Property management software for independent landlords — track rent, maintenance requests, leases and expenses in one place. Free to start.", - offers: { - "@type": "Offer", - price: "0", - priceCurrency: "USD", - }, + offers: planOffers, } const faqPage: Record = { diff --git a/instrumentation-client.ts b/instrumentation-client.ts new file mode 100644 index 0000000..0391345 --- /dev/null +++ b/instrumentation-client.ts @@ -0,0 +1,21 @@ +// Sentry initialization for the browser. Next.js loads this client-side +// instrumentation file automatically (Next 15.3+). +import * as Sentry from "@sentry/nextjs" + +const dsn = process.env.NEXT_PUBLIC_SENTRY_DSN + +Sentry.init({ + dsn, + // Inert until a DSN is configured. + enabled: !!dsn, + environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || process.env.NODE_ENV, + tracesSampleRate: process.env.NODE_ENV === "production" ? 0.2 : 1.0, + // Session Replay: record 10% of all sessions, and 100% of sessions with an + // error. Text is masked and media blocked so tenant/landlord PII isn't captured. + replaysSessionSampleRate: 0.1, + replaysOnErrorSampleRate: 1.0, + integrations: [Sentry.replayIntegration({ maskAllText: true, blockAllMedia: true })], +}) + +// Instrument client-side route transitions for tracing. +export const onRouterTransitionStart = Sentry.captureRouterTransitionStart diff --git a/instrumentation.ts b/instrumentation.ts index 859c531..2679f02 100644 --- a/instrumentation.ts +++ b/instrumentation.ts @@ -1,8 +1,19 @@ // Next.js instrumentation hook — runs once when the server process starts. -// Surfaces "silently disabled" integrations at boot so a misconfigured deploy is -// obvious in the logs instead of failing quietly at runtime. -export function register() { - // Only run in the Node.js server runtime (not edge/middleware) and only in prod. +// (1) Initializes Sentry for the active runtime, and (2) surfaces "silently +// disabled" integrations at boot so a misconfigured deploy is obvious in the +// logs instead of failing quietly at runtime. +import * as Sentry from "@sentry/nextjs" + +export async function register() { + // Initialize Sentry for whichever server runtime is booting. + if (process.env.NEXT_RUNTIME === "nodejs") { + await import("./sentry.server.config") + } + if (process.env.NEXT_RUNTIME === "edge") { + await import("./sentry.edge.config") + } + + // --- Startup env warnings (Node.js server runtime, production only) --- if (process.env.NEXT_RUNTIME !== "nodejs") return if (process.env.NODE_ENV !== "production") return @@ -33,4 +44,12 @@ export function register() { if (!process.env.OPENAI_API_KEY) { warn("OPENAI_API_KEY is NOT set — AI features will error when called.") } + + if (!process.env.SENTRY_DSN && !process.env.NEXT_PUBLIC_SENTRY_DSN) { + warn("SENTRY DSN is NOT set — error monitoring is disabled (no crash reports).") + } } + +// Capture errors thrown in nested React Server Components, route handlers, and +// server actions and report them to Sentry. +export const onRequestError = Sentry.captureRequestError diff --git a/lib/accounting/state.ts b/lib/accounting/state.ts index 238e519..628a123 100644 --- a/lib/accounting/state.ts +++ b/lib/accounting/state.ts @@ -1,22 +1,41 @@ import crypto from "crypto" -// Signed OAuth `state` (HMAC-SHA256) — carries the initiating owner + provider -// and is tamper-proof, so the callback can't be forged/CSRF'd. -const SECRET = process.env.BETTER_AUTH_SECRET ?? "dev-secret" +// Signed OAuth `state` (HMAC-SHA256) — carries the initiating owner + provider, +// a random nonce (bound to a cookie by the connect route for CSRF protection), +// and an issued-at timestamp so a leaked state can't be replayed indefinitely. -export function signState(data: { ownerId: string; provider: string }): string { - const payload = Buffer.from(JSON.stringify(data)).toString("base64url") - const sig = crypto.createHmac("sha256", SECRET).update(payload).digest("base64url") +const STATE_TTL_MS = 10 * 60 * 1000 // 10 minutes + +// Short-lived httpOnly cookie the connect route sets and the callback verifies +// against the state's nonce (binds the OAuth round-trip to the initiating browser). +export const OAUTH_NONCE_COOKIE = "acct_oauth_nonce" + +// No insecure fallback: signing/verifying state without the real secret would +// let anyone forge a state for any owner, so we fail closed (mirrors lib/crypto.ts). +function secret(): string { + const s = process.env.BETTER_AUTH_SECRET + if (!s) throw new Error("BETTER_AUTH_SECRET is not set — required to sign OAuth state") + return s +} + +export type OAuthState = { ownerId: string; provider: string; nonce: string } + +export function signState(data: OAuthState): string { + const payload = Buffer.from(JSON.stringify({ ...data, iat: Date.now() })).toString("base64url") + const sig = crypto.createHmac("sha256", secret()).update(payload).digest("base64url") return `${payload}.${sig}` } -export function verifyState(state: string): { ownerId: string; provider: string } | null { +export function verifyState(state: string): OAuthState | null { const [payload, sig] = state.split(".") if (!payload || !sig) return null - const expect = crypto.createHmac("sha256", SECRET).update(payload).digest("base64url") + const expect = crypto.createHmac("sha256", secret()).update(payload).digest("base64url") if (sig.length !== expect.length || !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expect))) return null try { - return JSON.parse(Buffer.from(payload, "base64url").toString("utf8")) + const obj = JSON.parse(Buffer.from(payload, "base64url").toString("utf8")) as OAuthState & { iat?: number } + if (!obj.iat || Date.now() - obj.iat > STATE_TTL_MS) return null + if (!obj.ownerId || !obj.provider || !obj.nonce) return null + return { ownerId: obj.ownerId, provider: obj.provider, nonce: obj.nonce } } catch { return null } diff --git a/lib/admin/audit.ts b/lib/admin/audit.ts index 644a5ee..ec672f5 100644 --- a/lib/admin/audit.ts +++ b/lib/admin/audit.ts @@ -12,6 +12,7 @@ export type AdminAction = | "delete_user" | "resend_verification" | "maintenance_mode" + | "ai_provider" /** * Append one immutable row to admin_audit_log. Call this for EVERY mutating diff --git a/lib/ai/anthropic.ts b/lib/ai/anthropic.ts new file mode 100644 index 0000000..d2d2491 --- /dev/null +++ b/lib/ai/anthropic.ts @@ -0,0 +1,15 @@ +import Anthropic from "@anthropic-ai/sdk" + +// Lazily construct the Anthropic client so `next build` does NOT require +// ANTHROPIC_API_KEY — it's only needed at runtime when the admin selects the +// Anthropic (Claude) provider for AI features. Mirrors lib/ai/client.ts. +let _anthropic: Anthropic | null = null + +export function getAnthropic(): Anthropic { + if (!_anthropic) { + const key = process.env.ANTHROPIC_API_KEY + if (!key) throw new Error("ANTHROPIC_API_KEY is not set") + _anthropic = new Anthropic({ apiKey: key }) + } + return _anthropic +} diff --git a/lib/ai/client.ts b/lib/ai/client.ts index b666e02..bc7a27a 100644 --- a/lib/ai/client.ts +++ b/lib/ai/client.ts @@ -1,5 +1,16 @@ import OpenAI from "openai" +// True when the server has an AI provider key (OpenAI OR Anthropic). AI routes +// check this up front and return a friendly 503 instead of throwing, matching +// how the other optional integrations (Stripe / SMTP / Turnstile) degrade when +// unconfigured. The active provider is chosen by an admin (see lib/ai/provider). +export function aiConfigured(): boolean { + return Boolean(process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY) +} + +export const AI_UNCONFIGURED_ERROR = + "AI features aren't configured on this server (no OpenAI or Anthropic API key). Ask your administrator to enable them." + // Lazily construct the OpenAI client so `next build` does NOT require // OPENAI_API_KEY — it's only needed at runtime. Call sites keep using // `openai.xxx` unchanged; the Proxy builds the real client on first access. diff --git a/lib/ai/provider.ts b/lib/ai/provider.ts new file mode 100644 index 0000000..c3db096 --- /dev/null +++ b/lib/ai/provider.ts @@ -0,0 +1,146 @@ +import { eq } from "drizzle-orm" +import type OpenAI from "openai" +import { db } from "@/lib/db" +import { app_settings } from "@/lib/db/schema" +import { openai } from "@/lib/ai/client" +import { getAnthropic } from "@/lib/ai/anthropic" + +// ============================================================================ +// AI provider abstraction — one call site for every AI feature, backed by +// EITHER OpenAI or Anthropic (Claude). The active provider is chosen by an admin +// in Settings → System (persisted in app_settings), falling back to whichever +// provider actually has an API key configured on the server. +// ============================================================================ + +export type AiProvider = "openai" | "anthropic" +export type AiRole = "system" | "user" | "assistant" +export type AiMessage = { role: AiRole; content: string } + +export const AI_PROVIDER_KEY = "ai_provider" + +// Models are env-overridable. Both default to each provider's cheapest tier to +// keep token spend low: OpenAI gpt-4o-mini, Anthropic Claude Haiku 4.5 ($1/$5 +// per 1M). Pin a stronger model via OPENAI_MODEL / ANTHROPIC_MODEL if desired. +export const OPENAI_MODEL = process.env.OPENAI_MODEL ?? "gpt-4o-mini" +export const ANTHROPIC_MODEL = process.env.ANTHROPIC_MODEL ?? "claude-haiku-4-5" + +export function openaiConfigured(): boolean { + return Boolean(process.env.OPENAI_API_KEY) +} +export function anthropicConfigured(): boolean { + return Boolean(process.env.ANTHROPIC_API_KEY) +} + +function isProvider(v: unknown): v is AiProvider { + return v === "openai" || v === "anthropic" +} + +/** The admin-selected provider (defaults to openai). Fails safe to openai. */ +export async function getAiProvider(): Promise { + try { + const row = await db.query.app_settings.findFirst({ + where: eq(app_settings.key, AI_PROVIDER_KEY), + }) + const v = (row?.value as { provider?: string } | null)?.provider + return isProvider(v) ? v : "openai" + } catch { + return "openai" + } +} + +/** Persist the admin's provider choice. Admin-gated by the calling action. */ +export async function setAiProvider(provider: AiProvider): Promise { + await db + .insert(app_settings) + .values({ key: AI_PROVIDER_KEY, value: { provider } }) + .onConflictDoUpdate({ + target: app_settings.key, + set: { value: { provider }, updated_at: new Date().toISOString() }, + }) +} + +/** + * The provider actually used for a request: the selected one, unless it has no + * API key on the server and the other provider does — then we fall back so AI + * features keep working after a provider switch even if the key isn't set yet. + */ +function resolveEffective(selected: AiProvider): AiProvider { + if (selected === "anthropic" && !anthropicConfigured() && openaiConfigured()) return "openai" + if (selected === "openai" && !openaiConfigured() && anthropicConfigured()) return "anthropic" + return selected +} + +/** Everything the admin UI needs to render the provider picker. */ +export async function aiProviderStatus() { + const selected = await getAiProvider() + return { + selected, + effective: resolveEffective(selected), + openaiConfigured: openaiConfigured(), + anthropicConfigured: anthropicConfigured(), + openaiModel: OPENAI_MODEL, + anthropicModel: ANTHROPIC_MODEL, + } +} + +/** Strip a ```json fenced code block, if the model wrapped its JSON in one. */ +function stripFences(s: string): string { + return s + .replace(/^\s*```(?:json)?\s*/i, "") + .replace(/```\s*$/i, "") + .trim() +} + +/** + * Provider-agnostic single-shot completion. Returns the model's text output. + * + * `json: true` asks for a JSON object (OpenAI uses response_format; both + * providers rely on the prompt saying "JSON only") and strips any code fences + * so the caller can `JSON.parse` the result directly. + */ +export async function aiComplete(opts: { + messages: AiMessage[] + maxTokens?: number + json?: boolean +}): Promise { + const provider = resolveEffective(await getAiProvider()) + const maxTokens = opts.maxTokens ?? 1024 + + let text: string + if (provider === "anthropic") { + // Anthropic takes a top-level `system`; the rest are user/assistant turns. + const system = opts.messages + .filter((m) => m.role === "system") + .map((m) => m.content) + .join("\n\n") + const convo = opts.messages + .filter((m) => m.role !== "system") + .map((m) => ({ role: (m.role === "assistant" ? "assistant" : "user") as "assistant" | "user", content: m.content })) + if (convo.length === 0) convo.push({ role: "user", content: system || "Continue." }) + + const res = await getAnthropic().messages.create({ + model: ANTHROPIC_MODEL, + max_tokens: maxTokens, + ...(system ? { system } : {}), + messages: convo, + }) + text = res.content.map((b) => (b.type === "text" ? b.text : "")).join("") + } else { + const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = opts.messages.map((m) => + m.role === "system" + ? { role: "system", content: m.content } + : m.role === "assistant" + ? { role: "assistant", content: m.content } + : { role: "user", content: m.content } + ) + const res = await openai.chat.completions.create({ + model: OPENAI_MODEL, + max_tokens: maxTokens, + messages, + ...(opts.json ? { response_format: { type: "json_object" as const } } : {}), + }) + text = res.choices[0]?.message?.content ?? "" + } + + return opts.json ? stripFences(text) : text +} diff --git a/lib/db/admin-queries.ts b/lib/db/admin-queries.ts index e372057..c96a0ff 100644 --- a/lib/db/admin-queries.ts +++ b/lib/db/admin-queries.ts @@ -254,6 +254,7 @@ export function getEnvHealth() { "STRIPE_WEBHOOK_SECRET", "NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY", "OPENAI_API_KEY", + "ANTHROPIC_API_KEY", "SMTP_HOST", "SMTP_USER", "GOOGLE_CLIENT_ID", diff --git a/lib/db/migrations/0010_esign_connections.sql b/lib/db/migrations/0010_esign_connections.sql new file mode 100644 index 0000000..7e47810 --- /dev/null +++ b/lib/db/migrations/0010_esign_connections.sql @@ -0,0 +1,17 @@ +CREATE TABLE "esign_connections" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" text NOT NULL, + "provider" text NOT NULL, + "access_token" text NOT NULL, + "refresh_token" text, + "expires_at" timestamp with time zone, + "account_id" text, + "base_uri" text, + "account_name" text, + "status" text DEFAULT 'active' NOT NULL, + "last_error" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "esign_connections" ADD CONSTRAINT "esign_connections_user_id_profiles_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."profiles"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/lib/db/migrations/meta/0010_snapshot.json b/lib/db/migrations/meta/0010_snapshot.json new file mode 100644 index 0000000..dd3cdb2 --- /dev/null +++ b/lib/db/migrations/meta/0010_snapshot.json @@ -0,0 +1,3548 @@ +{ + "id": "5b8a5779-88ab-4af3-abc9-f42d536346e2", + "prevId": "fff0a9bb-59c2-4555-8b31-08a6774204ef", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account_members": { + "name": "account_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "member_id": { + "name": "member_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "invite_token": { + "name": "invite_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()::text" + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "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": { + "account_members_owner_id_profiles_id_fk": { + "name": "account_members_owner_id_profiles_id_fk", + "tableFrom": "account_members", + "tableTo": "profiles", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "account_members_member_id_profiles_id_fk": { + "name": "account_members_member_id_profiles_id_fk", + "tableFrom": "account_members", + "tableTo": "profiles", + "columnsFrom": [ + "member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "account_members_invite_token_unique": { + "name": "account_members_invite_token_unique", + "nullsNotDistinct": false, + "columns": [ + "invite_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.accounting_connections": { + "name": "accounting_connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "realm_id": { + "name": "realm_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "org_name": { + "name": "org_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": { + "accounting_connections_user_id_profiles_id_fk": { + "name": "accounting_connections_user_id_profiles_id_fk", + "tableFrom": "accounting_connections", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.activity_log": { + "name": "activity_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_id": { + "name": "entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "activity_log_user_id_profiles_id_fk": { + "name": "activity_log_user_id_profiles_id_fk", + "tableFrom": "activity_log", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.admin_audit_log": { + "name": "admin_audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "admin_id": { + "name": "admin_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "admin_audit_log_admin_id_user_id_fk": { + "name": "admin_audit_log_admin_id_user_id_fk", + "tableFrom": "admin_audit_log", + "tableTo": "user", + "columnsFrom": [ + "admin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_predictions": { + "name": "ai_predictions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prediction": { + "name": "prediction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timeframe": { + "name": "timeframe", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "risk_level": { + "name": "risk_level", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "ai_predictions_user_id_profiles_id_fk": { + "name": "ai_predictions_user_id_profiles_id_fk", + "tableFrom": "ai_predictions", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_recommendations": { + "name": "ai_recommendations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impact": { + "name": "impact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "action_label": { + "name": "action_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action_data": { + "name": "action_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "applied_at": { + "name": "applied_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "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": { + "ai_recommendations_user_id_profiles_id_fk": { + "name": "ai_recommendations_user_id_profiles_id_fk", + "tableFrom": "ai_recommendations", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_prefix": { + "name": "key_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_profiles_id_fk": { + "name": "api_keys_user_id_profiles_id_fk", + "tableFrom": "api_keys", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_keys_key_hash_unique": { + "name": "api_keys_key_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "key_hash" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.app_settings": { + "name": "app_settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documents": { + "name": "documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_path": { + "name": "storage_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_type": { + "name": "file_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'other'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "documents_user_id_profiles_id_fk": { + "name": "documents_user_id_profiles_id_fk", + "tableFrom": "documents", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "documents_property_id_properties_id_fk": { + "name": "documents_property_id_properties_id_fk", + "tableFrom": "documents", + "tableTo": "properties", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "documents_tenant_id_tenants_id_fk": { + "name": "documents_tenant_id_tenants_id_fk", + "tableFrom": "documents", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.esign_connections": { + "name": "esign_connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_uri": { + "name": "base_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_name": { + "name": "account_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": { + "esign_connections_user_id_profiles_id_fk": { + "name": "esign_connections_user_id_profiles_id_fk", + "tableFrom": "esign_connections", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.expenses": { + "name": "expenses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "unit_id": { + "name": "unit_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "expense_date": { + "name": "expense_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "vendor": { + "name": "vendor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "receipt_url": { + "name": "receipt_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_recurring": { + "name": "is_recurring", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "recurrence": { + "name": "recurrence", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": { + "expenses_user_id_profiles_id_fk": { + "name": "expenses_user_id_profiles_id_fk", + "tableFrom": "expenses", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "expenses_property_id_properties_id_fk": { + "name": "expenses_property_id_properties_id_fk", + "tableFrom": "expenses", + "tableTo": "properties", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "expenses_unit_id_units_id_fk": { + "name": "expenses_unit_id_units_id_fk", + "tableFrom": "expenses", + "tableTo": "units", + "columnsFrom": [ + "unit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.follow_up_log": { + "name": "follow_up_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recipient_name": { + "name": "recipient_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recipient_email": { + "name": "recipient_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'sent'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "follow_up_log_user_id_profiles_id_fk": { + "name": "follow_up_log_user_id_profiles_id_fk", + "tableFrom": "follow_up_log", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "follow_up_log_rule_id_follow_up_rules_id_fk": { + "name": "follow_up_log_rule_id_follow_up_rules_id_fk", + "tableFrom": "follow_up_log", + "tableTo": "follow_up_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.follow_up_rules": { + "name": "follow_up_rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger_days": { + "name": "trigger_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "message_template": { + "name": "message_template", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "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": { + "follow_up_rules_user_id_profiles_id_fk": { + "name": "follow_up_rules_user_id_profiles_id_fk", + "tableFrom": "follow_up_rules", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inspections": { + "name": "inspections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "unit_id": { + "name": "unit_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "items": { + "name": "items", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": { + "inspections_user_id_profiles_id_fk": { + "name": "inspections_user_id_profiles_id_fk", + "tableFrom": "inspections", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "inspections_property_id_properties_id_fk": { + "name": "inspections_property_id_properties_id_fk", + "tableFrom": "inspections", + "tableTo": "properties", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "inspections_unit_id_units_id_fk": { + "name": "inspections_unit_id_units_id_fk", + "tableFrom": "inspections", + "tableTo": "units", + "columnsFrom": [ + "unit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.leases": { + "name": "leases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "unit_id": { + "name": "unit_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "lease_start": { + "name": "lease_start", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "lease_end": { + "name": "lease_end", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "rent_amount": { + "name": "rent_amount", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "security_deposit": { + "name": "security_deposit", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "lease_type": { + "name": "lease_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'fixed'" + }, + "document_url": { + "name": "document_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "auto_renew": { + "name": "auto_renew", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "reminder_60_sent": { + "name": "reminder_60_sent", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "reminder_30_sent": { + "name": "reminder_30_sent", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "reminder_7_sent": { + "name": "reminder_7_sent", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": { + "leases_user_id_profiles_id_fk": { + "name": "leases_user_id_profiles_id_fk", + "tableFrom": "leases", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "leases_tenant_id_tenants_id_fk": { + "name": "leases_tenant_id_tenants_id_fk", + "tableFrom": "leases", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "leases_property_id_properties_id_fk": { + "name": "leases_property_id_properties_id_fk", + "tableFrom": "leases", + "tableTo": "properties", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "leases_unit_id_units_id_fk": { + "name": "leases_unit_id_units_id_fk", + "tableFrom": "leases", + "tableTo": "units", + "columnsFrom": [ + "unit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.maintenance_requests": { + "name": "maintenance_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "property_id": { + "name": "property_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "unit_id": { + "name": "unit_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'general'" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "images": { + "name": "images", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "assigned_to": { + "name": "assigned_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "estimated_cost": { + "name": "estimated_cost", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "actual_cost": { + "name": "actual_cost", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resolution_notes": { + "name": "resolution_notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": { + "maintenance_requests_user_id_profiles_id_fk": { + "name": "maintenance_requests_user_id_profiles_id_fk", + "tableFrom": "maintenance_requests", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "maintenance_requests_tenant_id_tenants_id_fk": { + "name": "maintenance_requests_tenant_id_tenants_id_fk", + "tableFrom": "maintenance_requests", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "maintenance_requests_property_id_properties_id_fk": { + "name": "maintenance_requests_property_id_properties_id_fk", + "tableFrom": "maintenance_requests", + "tableTo": "properties", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "maintenance_requests_unit_id_units_id_fk": { + "name": "maintenance_requests_unit_id_units_id_fk", + "tableFrom": "maintenance_requests", + "tableTo": "units", + "columnsFrom": [ + "unit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications": { + "name": "notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recipient_email": { + "name": "recipient_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'sent'" + }, + "read": { + "name": "read", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "notifications_user_id_profiles_id_fk": { + "name": "notifications_user_id_profiles_id_fk", + "tableFrom": "notifications", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profiles": { + "name": "profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "full_name": { + "name": "full_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "company_name": { + "name": "company_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'starter'" + }, + "plan_expires_at": { + "name": "plan_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subscription_status": { + "name": "subscription_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "paypal_subscription_id": { + "name": "paypal_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_provider": { + "name": "billing_provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trial_ends_at": { + "name": "trial_ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "onboarding_completed": { + "name": "onboarding_completed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "brand_name": { + "name": "brand_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "brand_logo_url": { + "name": "brand_logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "brand_color": { + "name": "brand_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hide_powered_by": { + "name": "hide_powered_by", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "calendar_token": { + "name": "calendar_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "gen_random_uuid()::text" + }, + "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": { + "profiles_id_user_id_fk": { + "name": "profiles_id_user_id_fk", + "tableFrom": "profiles", + "tableTo": "user", + "columnsFrom": [ + "id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "profiles_stripe_customer_id_unique": { + "name": "profiles_stripe_customer_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stripe_customer_id" + ] + }, + "profiles_calendar_token_unique": { + "name": "profiles_calendar_token_unique", + "nullsNotDistinct": false, + "columns": [ + "calendar_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.properties": { + "name": "properties", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address_line1": { + "name": "address_line1", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address_line2": { + "name": "address_line2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "city": { + "name": "city", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postal_code": { + "name": "postal_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country": { + "name": "country", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'US'" + }, + "latitude": { + "name": "latitude", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "longitude": { + "name": "longitude", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "property_type": { + "name": "property_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'residential'" + }, + "total_units": { + "name": "total_units", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "image_url": { + "name": "image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": { + "properties_user_id_profiles_id_fk": { + "name": "properties_user_id_profiles_id_fk", + "tableFrom": "properties", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rent_payments": { + "name": "rent_payments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "unit_id": { + "name": "unit_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "amount": { + "name": "amount", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "due_date": { + "name": "due_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "paid_date": { + "name": "paid_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "payment_method": { + "name": "payment_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_payment_link_id": { + "name": "stripe_payment_link_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_payment_intent_id": { + "name": "stripe_payment_intent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reminder_sent_at": { + "name": "reminder_sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "late_fee_applied": { + "name": "late_fee_applied", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": { + "rent_payments_user_id_profiles_id_fk": { + "name": "rent_payments_user_id_profiles_id_fk", + "tableFrom": "rent_payments", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "rent_payments_tenant_id_tenants_id_fk": { + "name": "rent_payments_tenant_id_tenants_id_fk", + "tableFrom": "rent_payments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "rent_payments_property_id_properties_id_fk": { + "name": "rent_payments_property_id_properties_id_fk", + "tableFrom": "rent_payments", + "tableTo": "properties", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "rent_payments_unit_id_units_id_fk": { + "name": "rent_payments_unit_id_units_id_fk", + "tableFrom": "rent_payments", + "tableTo": "units", + "columnsFrom": [ + "unit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.signature_requests": { + "name": "signature_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lease_id": { + "name": "lease_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'sent'" + }, + "signer_email": { + "name": "signer_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signer_name": { + "name": "signer_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "document_name": { + "name": "document_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "signed_document_url": { + "name": "signed_document_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "signature_requests_user_id_profiles_id_fk": { + "name": "signature_requests_user_id_profiles_id_fk", + "tableFrom": "signature_requests", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "signature_requests_lease_id_leases_id_fk": { + "name": "signature_requests_lease_id_leases_id_fk", + "tableFrom": "signature_requests", + "tableTo": "leases", + "columnsFrom": [ + "lease_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tenants": { + "name": "tenants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "unit_id": { + "name": "unit_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "emergency_contact_name": { + "name": "emergency_contact_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "emergency_contact_phone": { + "name": "emergency_contact_phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "move_in_date": { + "name": "move_in_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "move_out_date": { + "name": "move_out_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "portal_token": { + "name": "portal_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()::text" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": { + "tenants_user_id_profiles_id_fk": { + "name": "tenants_user_id_profiles_id_fk", + "tableFrom": "tenants", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tenants_property_id_properties_id_fk": { + "name": "tenants_property_id_properties_id_fk", + "tableFrom": "tenants", + "tableTo": "properties", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tenants_unit_id_units_id_fk": { + "name": "tenants_unit_id_units_id_fk", + "tableFrom": "tenants", + "tableTo": "units", + "columnsFrom": [ + "unit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tenants_portal_token_unique": { + "name": "tenants_portal_token_unique", + "nullsNotDistinct": false, + "columns": [ + "portal_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.units": { + "name": "units", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "property_id": { + "name": "property_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "unit_number": { + "name": "unit_number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bedrooms": { + "name": "bedrooms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "bathrooms": { + "name": "bathrooms", + "type": "numeric(3, 1)", + "primaryKey": false, + "notNull": true, + "default": "1" + }, + "sq_ft": { + "name": "sq_ft", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rent_amount": { + "name": "rent_amount", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'vacant'" + }, + "current_tenant_id": { + "name": "current_tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": { + "units_property_id_properties_id_fk": { + "name": "units_property_id_properties_id_fk", + "tableFrom": "units", + "tableTo": "properties", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "units_user_id_profiles_id_fk": { + "name": "units_user_id_profiles_id_fk", + "tableFrom": "units", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_events": { + "name": "usage_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "usage_events_user_id_profiles_id_fk": { + "name": "usage_events_user_id_profiles_id_fk", + "tableFrom": "usage_events", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vendors": { + "name": "vendors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trade": { + "name": "trade", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": { + "vendors_user_id_profiles_id_fk": { + "name": "vendors_user_id_profiles_id_fk", + "tableFrom": "vendors", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vendors_property_id_properties_id_fk": { + "name": "vendors_property_id_properties_id_fk", + "tableFrom": "vendors", + "tableTo": "properties", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_deliveries": { + "name": "webhook_deliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "endpoint_id": { + "name": "endpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "response_body": { + "name": "response_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "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": { + "webhook_deliveries_user_id_profiles_id_fk": { + "name": "webhook_deliveries_user_id_profiles_id_fk", + "tableFrom": "webhook_deliveries", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_deliveries_endpoint_id_webhook_endpoints_id_fk": { + "name": "webhook_deliveries_endpoint_id_webhook_endpoints_id_fk", + "tableFrom": "webhook_deliveries", + "tableTo": "webhook_endpoints", + "columnsFrom": [ + "endpoint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_endpoints": { + "name": "webhook_endpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "events": { + "name": "events", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'dashboard'" + }, + "last_success_at": { + "name": "last_success_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error_at": { + "name": "last_error_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_count": { + "name": "failure_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "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": { + "webhook_endpoints_user_id_profiles_id_fk": { + "name": "webhook_endpoints_user_id_profiles_id_fk", + "tableFrom": "webhook_endpoints", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/lib/db/migrations/meta/_journal.json b/lib/db/migrations/meta/_journal.json index c1441ed..8521a1b 100644 --- a/lib/db/migrations/meta/_journal.json +++ b/lib/db/migrations/meta/_journal.json @@ -71,6 +71,13 @@ "when": 1782994066547, "tag": "0009_amusing_blackheart", "breakpoints": true + }, + { + "idx": 10, + "version": "7", + "when": 1783017593260, + "tag": "0010_esign_connections", + "breakpoints": true } ] } \ No newline at end of file diff --git a/lib/db/schema.ts b/lib/db/schema.ts index efc8ade..d7b8671 100644 --- a/lib/db/schema.ts +++ b/lib/db/schema.ts @@ -631,6 +631,32 @@ export const signature_requests = pgTable("signature_requests", { updated_at: updatedAt(), }) +// ============================================================ +// E-SIGN CONNECTIONS (per-landlord DocuSign OAuth / Dropbox Sign API key) +// ============================================================ +// One row per (owner, provider). Each landlord connects THEIR OWN e-signature +// account, so leases are sent from their brand with their audit trail. DocuSign +// uses OAuth (access + refresh tokens); Dropbox Sign uses an API key stored in +// `access_token`. All secrets are AES-256-GCM encrypted (see lib/crypto.ts). +export const esign_connections = pgTable("esign_connections", { + id: uuid("id").primaryKey().defaultRandom(), + user_id: text("user_id") + .notNull() + .references(() => profiles.id, { onDelete: "cascade" }), + provider: text("provider").$type<"docusign" | "dropbox_sign">().notNull(), + access_token: text("access_token").notNull(), // encrypted (DocuSign access token / Dropbox Sign API key) + refresh_token: text("refresh_token"), // encrypted (DocuSign only) + expires_at: tstz("expires_at"), + // DocuSign account id + base uri from /oauth/userinfo (null for Dropbox Sign). + account_id: text("account_id"), + base_uri: text("base_uri"), + account_name: text("account_name"), + status: text("status").$type<"active" | "error" | "revoked">().notNull().default("active"), + last_error: text("last_error"), + created_at: createdAt(), + updated_at: updatedAt(), +}) + // ============================================================ // WEBHOOK ENDPOINTS (outbound webhooks / Zapier integration) // ============================================================ diff --git a/lib/esign/credentials.ts b/lib/esign/credentials.ts new file mode 100644 index 0000000..a942d63 --- /dev/null +++ b/lib/esign/credentials.ts @@ -0,0 +1,104 @@ +import { and, eq } from "drizzle-orm" +import { db } from "@/lib/db" +import { esign_connections } from "@/lib/db/schema" +import { encrypt, decrypt } from "@/lib/crypto" +import { getAdapter } from "./registry" +import type { ESignCredentials, ESignProvider, ESignTokens } from "./types" + +// Per-owner e-sign connection storage + credential resolution. Mirrors +// lib/accounting/index.ts: tokens are AES-256-GCM encrypted at rest, decrypted +// on demand, and DocuSign access tokens are transparently refreshed near expiry. + +/** Upsert an encrypted connection for (owner, provider). */ +export async function saveEsignConnection(ownerId: string, provider: ESignProvider, tokens: ESignTokens) { + const values = { + user_id: ownerId, + provider, + access_token: encrypt(tokens.accessToken), + refresh_token: tokens.refreshToken ? encrypt(tokens.refreshToken) : null, + expires_at: tokens.expiresAt, + account_id: tokens.accountId, + base_uri: tokens.baseUri, + account_name: tokens.accountName, + status: "active" as const, + last_error: null, + } + const existing = await db.query.esign_connections.findFirst({ + where: and(eq(esign_connections.user_id, ownerId), eq(esign_connections.provider, provider)), + columns: { id: true }, + }) + if (existing) { + await db + .update(esign_connections) + .set({ ...values, updated_at: new Date().toISOString() }) + .where(eq(esign_connections.id, existing.id)) + } else { + await db.insert(esign_connections).values(values) + } +} + +export async function getEsignConnection(ownerId: string, provider: ESignProvider) { + return db.query.esign_connections.findFirst({ + where: and(eq(esign_connections.user_id, ownerId), eq(esign_connections.provider, provider)), + }) +} + +/** Owner-facing list — never leaks tokens. */ +export async function listEsignConnections(ownerId: string) { + const rows = await db.query.esign_connections.findMany({ where: eq(esign_connections.user_id, ownerId) }) + return rows.map((r) => ({ + provider: r.provider, + accountName: r.account_name, + status: r.status, + lastError: r.last_error, + })) +} + +export async function disconnectEsign(ownerId: string, provider: ESignProvider) { + await db + .delete(esign_connections) + .where(and(eq(esign_connections.user_id, ownerId), eq(esign_connections.provider, provider))) +} + +/** + * Resolve ready-to-use credentials for a connected account, refreshing the + * DocuSign access token first if it's near expiry. Returns null when the owner + * hasn't connected this provider. + */ +export async function resolveEsignCreds(ownerId: string, provider: ESignProvider): Promise { + const conn = await getEsignConnection(ownerId, provider) + if (!conn || conn.status === "revoked") return null + + let accessToken = decrypt(conn.access_token) + const refreshToken = conn.refresh_token ? decrypt(conn.refresh_token) : null + let accountId = conn.account_id + let baseUri = conn.base_uri + + const nearExpiry = conn.expires_at && new Date(conn.expires_at).getTime() - Date.now() < 5 * 60_000 + if (nearExpiry && refreshToken) { + const adapter = getAdapter(provider) + if (adapter) { + const next = await adapter.refresh(refreshToken) + // Account id / base uri are stable across refresh — keep the stored ones. + await saveEsignConnection(ownerId, provider, { + ...next, + accountId: conn.account_id, + baseUri: conn.base_uri, + accountName: conn.account_name, + }) + accessToken = next.accessToken + accountId = conn.account_id + baseUri = conn.base_uri + } + } + + return { provider, accessToken, refreshToken, accountId, baseUri } +} + +/** Flag a connection as errored (e.g. after a failed send/refresh). */ +export async function markEsignError(ownerId: string, provider: ESignProvider, message: string) { + await db + .update(esign_connections) + .set({ status: "error", last_error: message.slice(0, 500), updated_at: new Date().toISOString() }) + .where(and(eq(esign_connections.user_id, ownerId), eq(esign_connections.provider, provider))) +} diff --git a/lib/esign/docusign.ts b/lib/esign/docusign.ts index f6db888..5c68666 100644 --- a/lib/esign/docusign.ts +++ b/lib/esign/docusign.ts @@ -1,44 +1,139 @@ -import type { ESignAdapter, SendParams, WebhookResult } from "./types" +import type { ESignAdapter, ESignCredentials, ESignTokens, SendParams } from "./types" +import { esignRedirectUri } from "./types" -// DocuSign eSignature REST API. Uses a pre-obtained access token (via JWT grant -// or OAuth) — set DOCUSIGN_ACCESS_TOKEN / DOCUSIGN_ACCOUNT_ID / DOCUSIGN_BASE_URI. -// Docs: https://developers.docusign.com/docs/esign-rest-api/reference/envelopes/envelopes/create/ -const ACCESS_TOKEN = process.env.DOCUSIGN_ACCESS_TOKEN ?? "" -const ACCOUNT_ID = process.env.DOCUSIGN_ACCOUNT_ID ?? "" -const BASE_URI = (process.env.DOCUSIGN_BASE_URI ?? "https://demo.docusign.net").replace(/\/+$/, "") +// DocuSign eSignature via per-landlord OAuth (Authorization Code Grant). +// The OPERATOR registers one DocuSign app and sets these; each LANDLORD then +// connects their own DocuSign account through it. +// DOCUSIGN_CLIENT_ID / DOCUSIGN_CLIENT_SECRET — the app's integration key + secret +// DOCUSIGN_OAUTH_BASE — "account-d.docusign.com" (demo) or "account.docusign.com" (prod) +const CLIENT_ID = process.env.DOCUSIGN_CLIENT_ID ?? "" +const CLIENT_SECRET = process.env.DOCUSIGN_CLIENT_SECRET ?? "" +const OAUTH_BASE = (process.env.DOCUSIGN_OAUTH_BASE ?? "account-d.docusign.com").replace(/^https?:\/\//, "").replace(/\/+$/, "") + +function basicAuth() { + return "Basic " + Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString("base64") +} function extOf(name: string): string { const e = name.split(".").pop()?.toLowerCase() return e && /^(pdf|docx?|png|jpe?g)$/.test(e) ? e : "pdf" } +/** Map a DocuSign envelope/event status string to one of our terminal statuses. */ +function mapStatus(raw: string): "signed" | "declined" | "voided" | null { + const s = raw.toLowerCase() + if (s.includes("completed") || s.includes("signed")) return "signed" + if (s.includes("declined")) return "declined" + if (s.includes("voided")) return "voided" + return null +} + +async function tokenRequest(form: Record): Promise<{ access_token: string; refresh_token: string; expires_in: number }> { + const res = await fetch(`https://${OAUTH_BASE}/oauth/token`, { + method: "POST", + headers: { Authorization: basicAuth(), "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" }, + body: new URLSearchParams(form), + }) + if (!res.ok) throw new Error(`DocuSign token error ${res.status}: ${(await res.text()).slice(0, 300)}`) + return res.json() +} + +async function userInfo(accessToken: string): Promise<{ accountId: string | null; baseUri: string | null; accountName: string | null }> { + const res = await fetch(`https://${OAUTH_BASE}/oauth/userinfo`, { + headers: { Authorization: `Bearer ${accessToken}`, Accept: "application/json" }, + }) + if (!res.ok) throw new Error(`DocuSign userinfo ${res.status}`) + const j = (await res.json()) as { accounts?: { account_id: string; base_uri: string; account_name: string; is_default: boolean }[] } + const acct = j.accounts?.find((a) => a.is_default) ?? j.accounts?.[0] + return { accountId: acct?.account_id ?? null, baseUri: acct?.base_uri ?? null, accountName: acct?.account_name ?? null } +} + +/** REST API base for the envelopes API, e.g. https://na3.docusign.net/restapi/v2.1/accounts/ */ +function apiBase(creds: ESignCredentials): string { + return `${(creds.baseUri ?? "").replace(/\/+$/, "")}/restapi/v2.1/accounts/${creds.accountId}` +} + export const docusign: ESignAdapter = { id: "docusign", label: "DocuSign", - configured: () => Boolean(ACCESS_TOKEN && ACCOUNT_ID), + kind: "oauth", + available: () => Boolean(CLIENT_ID && CLIENT_SECRET), - async send({ document, documentName, signerEmail, signerName, subject }: SendParams) { + getAuthUrl(state) { + const p = new URLSearchParams({ + response_type: "code", + // `extended` is required to receive a refresh token. + scope: "signature extended", + client_id: CLIENT_ID, + redirect_uri: esignRedirectUri("docusign"), + state, + }) + return `https://${OAUTH_BASE}/oauth/auth?${p.toString()}` + }, + + async exchangeCode(code): Promise { + const t = await tokenRequest({ grant_type: "authorization_code", code }) + const info = await userInfo(t.access_token) + return { + accessToken: t.access_token, + refreshToken: t.refresh_token, + expiresAt: new Date(Date.now() + t.expires_in * 1000).toISOString(), + accountId: info.accountId, + baseUri: info.baseUri, + accountName: info.accountName, + } + }, + + async refresh(refreshToken): Promise { + const t = await tokenRequest({ grant_type: "refresh_token", refresh_token: refreshToken }) + // Account id / base uri are stable across refreshes; the resolver re-attaches them. + return { + accessToken: t.access_token, + refreshToken: t.refresh_token, + expiresAt: new Date(Date.now() + t.expires_in * 1000).toISOString(), + accountId: null, + baseUri: null, + accountName: null, + } + }, + + async connectApiKey(): Promise { + throw new Error("DocuSign connects via OAuth, not an API key") + }, + + async send(creds, p: SendParams) { const envelope = { - emailSubject: subject, + emailSubject: p.subject, status: "sent", - documents: [{ documentBase64: document.toString("base64"), name: documentName, fileExtension: extOf(documentName), documentId: "1" }], + documents: [{ documentBase64: p.document.toString("base64"), name: p.documentName, fileExtension: extOf(p.documentName), documentId: "1" }], recipients: { signers: [ { - email: signerEmail, - name: signerName, + email: p.signerEmail, + name: p.signerName, recipientId: "1", routingOrder: "1", - // Default sign placement (bottom of page 1). Use a template/anchor - // string for precise field placement in production. tabs: { signHereTabs: [{ documentId: "1", pageNumber: "1", xPosition: "100", yPosition: "650" }] }, }, ], }, + // Envelope-level Connect: DocuSign pings our webhook on completion so we + // pull the authoritative status. No account-level Connect config needed. + eventNotification: { + url: p.webhookUrl, + loggingEnabled: "true", + requireAcknowledgment: "true", + envelopeEvents: [ + { envelopeEventStatusCode: "completed" }, + { envelopeEventStatusCode: "declined" }, + { envelopeEventStatusCode: "voided" }, + ], + eventData: { version: "restv2.1" }, + }, } - const res = await fetch(`${BASE_URI}/restapi/v2.1/accounts/${ACCOUNT_ID}/envelopes`, { + const res = await fetch(`${apiBase(creds)}/envelopes`, { method: "POST", - headers: { Authorization: `Bearer ${ACCESS_TOKEN}`, "Content-Type": "application/json" }, + headers: { Authorization: `Bearer ${creds.accessToken}`, "Content-Type": "application/json" }, body: JSON.stringify(envelope), }) if (!res.ok) throw new Error(`DocuSign ${res.status}: ${(await res.text()).slice(0, 300)}`) @@ -47,19 +142,32 @@ export const docusign: ESignAdapter = { return { externalId: j.envelopeId } }, - parseWebhook(body): WebhookResult | null { - // DocuSign Connect (JSON format) payload. + peekExternalId(body): string | null { try { - const j = JSON.parse(body) as { event?: string; data?: { envelopeId?: string; envelopeSummary?: { status?: string } } } - const externalId = j.data?.envelopeId - const status = (j.data?.envelopeSummary?.status ?? j.event ?? "").toLowerCase() - if (!externalId) return null - if (status.includes("completed") || status.includes("signed")) return { externalId, status: "signed" } - if (status.includes("declined")) return { externalId, status: "declined" } - if (status.includes("voided")) return { externalId, status: "voided" } - return null + const j = JSON.parse(body) as { data?: { envelopeId?: string } } + return j.data?.envelopeId ?? null } catch { return null } }, + + // We never trust the webhook body's status. Instead we fetch the envelope from + // DocuSign with the owner's own OAuth token — a forged webhook can at most make + // us re-read the real status, never fabricate a "signed". + async verifyAndGetStatus(creds, externalId) { + const res = await fetch(`${apiBase(creds)}/envelopes/${externalId}`, { + headers: { Authorization: `Bearer ${creds.accessToken}`, Accept: "application/json" }, + }) + if (!res.ok) return null + const j = (await res.json()) as { status?: string } + return j.status ? mapStatus(j.status) : null + }, + + async getSignedDocument(creds, externalId): Promise { + const res = await fetch(`${apiBase(creds)}/envelopes/${externalId}/documents/combined`, { + headers: { Authorization: `Bearer ${creds.accessToken}`, Accept: "application/pdf" }, + }) + if (!res.ok) return null + return Buffer.from(await res.arrayBuffer()) + }, } diff --git a/lib/esign/dropbox-sign.ts b/lib/esign/dropbox-sign.ts index 9d64935..d70ee18 100644 --- a/lib/esign/dropbox-sign.ts +++ b/lib/esign/dropbox-sign.ts @@ -1,30 +1,83 @@ -import type { ESignAdapter, SendParams, WebhookResult } from "./types" +import crypto from "crypto" +import type { ESignAdapter, ESignTokens, SendParams } from "./types" -// Dropbox Sign (formerly HelloSign). API-key auth. Docs: -// https://developers.hellosign.com/api/reference/operation/signatureRequestSend/ -const API_KEY = process.env.DROPBOX_SIGN_API_KEY ?? "" -const TEST_MODE = process.env.DROPBOX_SIGN_TEST_MODE === "true" ? "1" : "0" +// Dropbox Sign (formerly HelloSign). Per-landlord API-key auth — each landlord +// pastes their own API key (no platform app credentials needed). Docs: +// https://developers.hellosign.com/api/reference/ const BASE = "https://api.hellosign.com/v3" +const TEST_MODE = process.env.DROPBOX_SIGN_TEST_MODE === "true" ? "1" : "0" -function auth() { - return "Basic " + Buffer.from(`${API_KEY}:`).toString("base64") +function authFor(apiKey: string) { + return "Basic " + Buffer.from(`${apiKey}:`).toString("base64") +} + +/** Verify the `event_hash` (hex HMAC-SHA256 of event_time+event_type, key = API key). */ +function verifyEventHash(apiKey: string, ev?: { event_type?: string; event_time?: string; event_hash?: string }): boolean { + if (!apiKey || !ev?.event_type || !ev?.event_time || !ev?.event_hash) return false + const expected = crypto.createHmac("sha256", apiKey).update(ev.event_time + ev.event_type).digest("hex") + const a = Buffer.from(ev.event_hash) + const b = Buffer.from(expected) + return a.length === b.length && crypto.timingSafeEqual(a, b) +} + +type DbxEvent = { + event?: { event_type?: string; event_time?: string; event_hash?: string } + signature_request?: { signature_request_id?: string } +} + +function parseBody(body: string): DbxEvent | null { + try { + // Dropbox Sign posts multipart form-data with a `json` field (or raw JSON). + const m = body.match(/name="json"\r?\n\r?\n([\s\S]*?)\r?\n--/) ?? body.match(/^(\{[\s\S]*\})\s*$/) + return JSON.parse(m ? m[1] : body) + } catch { + return null + } } export const dropboxSign: ESignAdapter = { id: "dropbox_sign", label: "Dropbox Sign", - configured: () => Boolean(API_KEY), + kind: "apikey", + available: () => true, // landlord brings their own key; no operator setup required - async send({ document, documentName, signerEmail, signerName, subject, message }: SendParams) { + getAuthUrl(): string { + throw new Error("Dropbox Sign connects with an API key, not OAuth") + }, + async exchangeCode(): Promise { + throw new Error("Dropbox Sign connects with an API key, not OAuth") + }, + async refresh(): Promise { + throw new Error("Dropbox Sign API keys don't expire") + }, + + async connectApiKey(apiKey): Promise { + const key = apiKey.trim() + if (!key) throw new Error("Enter your Dropbox Sign API key") + const res = await fetch(`${BASE}/account`, { headers: { Authorization: authFor(key) } }) + if (res.status === 401 || res.status === 403) throw new Error("That API key was rejected by Dropbox Sign") + if (!res.ok) throw new Error(`Dropbox Sign ${res.status}: could not validate the API key`) + const j = (await res.json()) as { account?: { email_address?: string } } + return { + accessToken: key, + refreshToken: null, + expiresAt: null, + accountId: null, + baseUri: null, + accountName: j.account?.email_address ?? "Dropbox Sign account", + } + }, + + async send(creds, p: SendParams) { const fd = new FormData() - fd.append("subject", subject) - fd.append("message", message) + fd.append("subject", p.subject) + fd.append("message", p.message) fd.append("test_mode", TEST_MODE) - fd.append("signers[0][email_address]", signerEmail) - fd.append("signers[0][name]", signerName) - fd.append("file[0]", new Blob([new Uint8Array(document)], { type: "application/pdf" }), documentName) + fd.append("signers[0][email_address]", p.signerEmail) + fd.append("signers[0][name]", p.signerName) + fd.append("file[0]", new Blob([new Uint8Array(p.document)], { type: "application/pdf" }), p.documentName) - const res = await fetch(`${BASE}/signature_request/send`, { method: "POST", headers: { Authorization: auth() }, body: fd }) + const res = await fetch(`${BASE}/signature_request/send`, { method: "POST", headers: { Authorization: authFor(creds.accessToken) }, body: fd }) if (!res.ok) throw new Error(`Dropbox Sign ${res.status}: ${(await res.text()).slice(0, 300)}`) const j = (await res.json()) as { signature_request?: { signature_request_id?: string } } const id = j.signature_request?.signature_request_id @@ -32,22 +85,30 @@ export const dropboxSign: ESignAdapter = { return { externalId: id } }, - parseWebhook(body): WebhookResult | null { - // Dropbox Sign posts multipart form-data with a `json` field. - let event: { event?: { event_type?: string }; signature_request?: { signature_request_id?: string } } - try { - // Extract the JSON payload whether sent raw or as a form field. - const m = body.match(/name="json"\r?\n\r?\n([\s\S]*?)\r?\n--/) ?? body.match(/^(\{[\s\S]*\})\s*$/) - event = JSON.parse(m ? m[1] : body) - } catch { - return null + peekExternalId(body): string | null { + return parseBody(body)?.signature_request?.signature_request_id ?? null + }, + + async verifyAndGetStatus(creds, _externalId, body): Promise<"signed" | "declined" | "voided" | null> { + const ev = parseBody(body) + if (!ev || !verifyEventHash(creds.accessToken, ev.event)) return null + switch (ev.event?.event_type) { + case "signature_request_all_signed": + return "signed" + case "signature_request_declined": + return "declined" + case "signature_request_canceled": + return "voided" + default: + return null } - const type = event.event?.event_type - const externalId = event.signature_request?.signature_request_id - if (!externalId || !type) return null - if (type === "signature_request_all_signed") return { externalId, status: "signed" } - if (type === "signature_request_declined") return { externalId, status: "declined" } - if (type === "signature_request_canceled") return { externalId, status: "voided" } - return null + }, + + async getSignedDocument(creds, externalId): Promise { + const res = await fetch(`${BASE}/signature_request/files/${externalId}?file_type=pdf`, { + headers: { Authorization: authFor(creds.accessToken) }, + }) + if (!res.ok) return null + return Buffer.from(await res.arrayBuffer()) }, } diff --git a/lib/esign/index.ts b/lib/esign/index.ts index 5917d9c..55a9c57 100644 --- a/lib/esign/index.ts +++ b/lib/esign/index.ts @@ -1,42 +1,48 @@ import { and, desc, eq } from "drizzle-orm" import { db } from "@/lib/db" import { leases, signature_requests } from "@/lib/db/schema" -import { readFile } from "@/lib/storage" -import { docusign } from "./docusign" -import { dropboxSign } from "./dropbox-sign" -import type { ESignAdapter, ESignProvider } from "./types" +import { readFile, saveBuffer } from "@/lib/storage" +import { getAdapter } from "./registry" +import { resolveEsignCreds, markEsignError } from "./credentials" +import type { ESignProvider } from "./types" export type { ESignProvider } from "./types" +export { getAdapter, listEsignAdapters } from "./registry" +export { + resolveEsignCreds, + listEsignConnections, + getEsignConnection, + saveEsignConnection, + disconnectEsign, +} from "./credentials" -const ADAPTERS: Record = { docusign, dropbox_sign: dropboxSign } +const FILES_PREFIX = "/api/files/" -export function getAdapter(id: string): ESignAdapter | null { - return id === "docusign" || id === "dropbox_sign" ? ADAPTERS[id] : null -} - -export function listAdapters() { - return (Object.keys(ADAPTERS) as ESignProvider[]).map((id) => ({ id, label: ADAPTERS[id].label, configured: ADAPTERS[id].configured() })) -} - -export function anyEsignConfigured(): boolean { - return listAdapters().some((a) => a.configured) +/** The webhook URL a provider should call back — used for DocuSign envelope-level Connect. */ +function webhookUrl(provider: ESignProvider): string { + const base = (process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000").replace(/\/+$/, "") + return `${base}/api/esign/${provider}/webhook` } +/** Read the lease's stored document. Only /api/files keys are allowed (no SSRF). */ async function getDocumentBytes(documentUrl: string): Promise<{ bytes: Buffer; name: string }> { - const prefix = "/api/files/" - if (documentUrl.startsWith(prefix)) { - const key = documentUrl.slice(prefix.length) - return { bytes: await readFile(key), name: key.split("/").pop() ?? "lease.pdf" } + if (!documentUrl.startsWith(FILES_PREFIX)) { + throw new Error("Lease document must be an uploaded file") } - const res = await fetch(documentUrl) - if (!res.ok) throw new Error("Could not fetch the lease document") - return { bytes: Buffer.from(await res.arrayBuffer()), name: documentUrl.split("/").pop()?.split("?")[0] ?? "lease.pdf" } + const key = documentUrl.slice(FILES_PREFIX.length) + return { bytes: await readFile(key), name: key.split("/").pop() ?? "lease.pdf" } } +/** + * Send a lease for signature through the owner's OWN connected account. + * Requires the provider to be connected (per-user OAuth / API key). + */ export async function sendLeaseForSignature(ownerId: string, leaseId: string, provider: ESignProvider) { const adapter = getAdapter(provider) if (!adapter) throw new Error("Unknown provider") - if (!adapter.configured()) throw new Error(`${adapter.label} is not configured`) + + const creds = await resolveEsignCreds(ownerId, provider) + if (!creds) throw new Error(`Connect your ${adapter.label} account in Settings → Integrations first`) const lease = await db.query.leases.findFirst({ where: and(eq(leases.id, leaseId), eq(leases.user_id, ownerId)), @@ -51,13 +57,14 @@ export async function sendLeaseForSignature(ownerId: string, leaseId: string, pr const { bytes, name } = await getDocumentBytes(lease.document_url) try { - const { externalId } = await adapter.send({ + const { externalId } = await adapter.send(creds, { document: bytes, documentName: name, signerEmail: email, signerName, subject: "Please sign your lease agreement", message: "Your landlord has sent your lease agreement for electronic signature.", + webhookUrl: webhookUrl(provider), }) const [row] = await db .insert(signature_requests) @@ -66,6 +73,7 @@ export async function sendLeaseForSignature(ownerId: string, leaseId: string, pr return row } catch (e) { const msg = (e as Error).message.slice(0, 500) + await markEsignError(ownerId, provider, msg) await db .insert(signature_requests) .values({ user_id: ownerId, lease_id: leaseId, provider, status: "error", signer_email: email, signer_name: signerName, document_name: name, last_error: msg }) @@ -80,18 +88,53 @@ export async function listRequestsForLease(ownerId: string, leaseId: string) { }) } -/** Update a request's status from an inbound provider webhook. */ +/** + * Process an inbound provider webhook. The body is UNTRUSTED: we use it only to + * find which signature request (and therefore which owner + credentials) it + * concerns, then authenticate the event via the adapter (DocuSign pull-verify / + * Dropbox HMAC) before updating status and archiving the signed document. + */ export async function handleEsignWebhook(provider: string, body: string, headers: Headers) { const adapter = getAdapter(provider) if (!adapter) return - const result = adapter.parseWebhook(body, headers) - if (!result) return + + const externalId = adapter.peekExternalId(body) + if (!externalId) return + + const reqRow = await db.query.signature_requests.findFirst({ + where: eq(signature_requests.external_id, externalId), + columns: { id: true, user_id: true, status: true }, + }) + if (!reqRow) return + + const creds = await resolveEsignCreds(reqRow.user_id, provider as ESignProvider) + if (!creds) return + + const status = await adapter.verifyAndGetStatus(creds, externalId, body, headers) + if (!status) return + await db .update(signature_requests) .set({ - status: result.status, - completed_at: result.status === "signed" ? new Date().toISOString() : null, + status, + completed_at: status === "signed" ? new Date().toISOString() : null, updated_at: new Date().toISOString(), }) - .where(eq(signature_requests.external_id, result.externalId)) + .where(eq(signature_requests.id, reqRow.id)) + + // Archive the executed document so the landlord can download the signed copy. + if (status === "signed") { + try { + const bytes = await adapter.getSignedDocument(creds, externalId) + if (bytes && bytes.length) { + const { key } = await saveBuffer(bytes, { userId: reqRow.user_id, scope: "esign", ext: "pdf" }) + await db + .update(signature_requests) + .set({ signed_document_url: `${FILES_PREFIX}${key}` }) + .where(eq(signature_requests.id, reqRow.id)) + } + } catch { + // Best-effort — status is already recorded. + } + } } diff --git a/lib/esign/registry.ts b/lib/esign/registry.ts new file mode 100644 index 0000000..99b0d23 --- /dev/null +++ b/lib/esign/registry.ts @@ -0,0 +1,21 @@ +import { docusign } from "./docusign" +import { dropboxSign } from "./dropbox-sign" +import type { ESignAdapter, ESignProvider } from "./types" + +// Adapter registry — dependency-free (no db) so it can be imported anywhere, +// including the credential resolver, without creating import cycles. +const ADAPTERS: Record = { docusign, dropbox_sign: dropboxSign } + +export function getAdapter(id: string): ESignAdapter | null { + return id === "docusign" || id === "dropbox_sign" ? ADAPTERS[id] : null +} + +/** Providers the platform can offer, with their connect style + availability. */ +export function listEsignAdapters() { + return (Object.keys(ADAPTERS) as ESignProvider[]).map((id) => ({ + id, + label: ADAPTERS[id].label, + kind: ADAPTERS[id].kind, + available: ADAPTERS[id].available(), + })) +} diff --git a/lib/esign/state.ts b/lib/esign/state.ts new file mode 100644 index 0000000..7026797 --- /dev/null +++ b/lib/esign/state.ts @@ -0,0 +1,39 @@ +import crypto from "crypto" + +// Signed OAuth `state` for the e-sign connect flow — carries the initiating +// owner + provider + a random nonce (bound to a cookie by the connect route), +// plus an issued-at so a leaked state can't be replayed. Mirrors the hardened +// accounting OAuth state; fails closed if BETTER_AUTH_SECRET is missing. + +const STATE_TTL_MS = 10 * 60 * 1000 // 10 minutes + +export const ESIGN_NONCE_COOKIE = "esign_oauth_nonce" + +function secret(): string { + const s = process.env.BETTER_AUTH_SECRET + if (!s) throw new Error("BETTER_AUTH_SECRET is not set — required to sign OAuth state") + return s +} + +export type EsignOAuthState = { ownerId: string; provider: string; nonce: string } + +export function signState(data: EsignOAuthState): string { + const payload = Buffer.from(JSON.stringify({ ...data, iat: Date.now() })).toString("base64url") + const sig = crypto.createHmac("sha256", secret()).update(payload).digest("base64url") + return `${payload}.${sig}` +} + +export function verifyState(state: string): EsignOAuthState | null { + const [payload, sig] = state.split(".") + if (!payload || !sig) return null + const expect = crypto.createHmac("sha256", secret()).update(payload).digest("base64url") + if (sig.length !== expect.length || !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expect))) return null + try { + const obj = JSON.parse(Buffer.from(payload, "base64url").toString("utf8")) as EsignOAuthState & { iat?: number } + if (!obj.iat || Date.now() - obj.iat > STATE_TTL_MS) return null + if (!obj.ownerId || !obj.provider || !obj.nonce) return null + return { ownerId: obj.ownerId, provider: obj.provider, nonce: obj.nonce } + } catch { + return null + } +} diff --git a/lib/esign/types.ts b/lib/esign/types.ts index cd439b0..07282d9 100644 --- a/lib/esign/types.ts +++ b/lib/esign/types.ts @@ -1,5 +1,27 @@ export type ESignProvider = "docusign" | "dropbox_sign" +export type ESignStatus = "signed" | "declined" | "voided" + +/** Result of connecting an account (OAuth exchange or API-key validation). */ +export interface ESignTokens { + accessToken: string + refreshToken: string | null + /** ISO expiry of the access token, or null (API keys don't expire). */ + expiresAt: string | null + accountId: string | null + baseUri: string | null + accountName: string | null +} + +/** Decrypted, ready-to-use credentials for a single connected account. */ +export interface ESignCredentials { + provider: ESignProvider + accessToken: string + refreshToken?: string | null + accountId?: string | null + baseUri?: string | null +} + export interface SendParams { document: Buffer documentName: string @@ -7,20 +29,54 @@ export interface SendParams { signerName: string subject: string message: string -} - -export interface WebhookResult { - externalId: string - status: "signed" | "declined" | "voided" + /** Our callback the provider should ping on status changes (DocuSign envelope-level Connect). */ + webhookUrl: string } export interface ESignAdapter { id: ESignProvider label: string - /** True when this provider's credentials are configured in env. */ - configured(): boolean - /** Send a document for signature; returns the provider's request/envelope id. */ - send(p: SendParams): Promise<{ externalId: string }> - /** Parse an inbound webhook body into a status update (or null to ignore). */ - parseWebhook(body: string, headers: Headers): WebhookResult | null + /** "oauth" → connect via redirect; "apikey" → connect by pasting a key. */ + kind: "oauth" | "apikey" + /** + * True when the platform can offer this provider. OAuth providers need the + * operator's app credentials (client id/secret); API-key providers are always + * available because the landlord brings their own key. + */ + available(): boolean + + // ── OAuth providers (DocuSign) ──────────────────────────────────────────── + getAuthUrl(state: string): string + exchangeCode(code: string): Promise + refresh(refreshToken: string): Promise + + // ── API-key providers (Dropbox Sign) ────────────────────────────────────── + /** Validate a pasted API key and return a token set to store. */ + connectApiKey(apiKey: string): Promise + + // ── Common ──────────────────────────────────────────────────────────────── + /** Send a document for signature; returns the provider's envelope/request id. */ + send(creds: ESignCredentials, params: SendParams): Promise<{ externalId: string }> + /** Extract the external id from an inbound (still UNVERIFIED) webhook body, for owner lookup. */ + peekExternalId(body: string): string | null + /** + * Authenticate an inbound webhook and return the authoritative status. + * DocuSign pull-verifies by fetching the envelope with the owner's token; + * Dropbox Sign HMAC-verifies the body with the account API key. Returns null + * if the event isn't authentic or isn't a terminal status we track. + */ + verifyAndGetStatus( + creds: ESignCredentials, + externalId: string, + body: string, + headers: Headers + ): Promise + /** Download the completed/executed document, or null if unavailable. */ + getSignedDocument(creds: ESignCredentials, externalId: string): Promise +} + +/** The OAuth callback URL for a provider (must match what's registered in the provider app). */ +export function esignRedirectUri(provider: ESignProvider): string { + const base = (process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000").replace(/\/+$/, "") + return `${base}/api/esign/${provider}/callback` } diff --git a/lib/paypal/checkout.ts b/lib/paypal/checkout.ts deleted file mode 100644 index e6c8692..0000000 --- a/lib/paypal/checkout.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { paypalFetch } from "./client" - -// We encode the app user id + target plan into PayPal's `custom_id` so webhooks -// and the return handler can resolve who/what a subscription or order is for, -// without trusting query params. Format: ":". -export function encodeCustomId(userId: string, plan: string): string { - return `${userId}:${plan}` -} -export function decodeCustomId(customId: string | null | undefined): { userId: string; plan: string } | null { - if (!customId) return null - const idx = customId.lastIndexOf(":") - if (idx <= 0) return null - return { userId: customId.slice(0, idx), plan: customId.slice(idx + 1) } -} - -function approveUrl(links: Array<{ rel: string; href: string }> | undefined): string | undefined { - return links?.find((l) => l.rel === "approve" || l.rel === "payer-action")?.href -} - -const BRAND = "Property Management Network" - -/** Create a recurring subscription; returns its id + the PayPal approval URL. */ -export async function createSubscription(params: { - planId: string - userId: string - plan: string - email?: string | null - returnUrl: string - cancelUrl: string -}): Promise<{ id: string; approveUrl?: string }> { - const res = await paypalFetch("/v1/billing/subscriptions", { - method: "POST", - body: JSON.stringify({ - plan_id: params.planId, - custom_id: encodeCustomId(params.userId, params.plan), - subscriber: params.email ? { email_address: params.email } : undefined, - application_context: { - brand_name: BRAND, - user_action: "SUBSCRIBE_NOW", - shipping_preference: "NO_SHIPPING", - return_url: params.returnUrl, - cancel_url: params.cancelUrl, - }, - }), - }) - if (!res.ok) throw new Error(`PayPal createSubscription failed: ${res.status} ${await res.text().catch(() => "")}`) - const json = (await res.json()) as { id: string; links?: Array<{ rel: string; href: string }> } - return { id: json.id, approveUrl: approveUrl(json.links) } -} - -/** Create a one-time order (used for the Lifetime plan). */ -export async function createOrder(params: { - amount: number - userId: string - plan: string - returnUrl: string - cancelUrl: string -}): Promise<{ id: string; approveUrl?: string }> { - const res = await paypalFetch("/v2/checkout/orders", { - method: "POST", - body: JSON.stringify({ - intent: "CAPTURE", - purchase_units: [ - { - amount: { currency_code: "USD", value: params.amount.toFixed(2) }, - custom_id: encodeCustomId(params.userId, params.plan), - description: `${BRAND} — Lifetime`, - }, - ], - application_context: { - brand_name: BRAND, - user_action: "PAY_NOW", - shipping_preference: "NO_SHIPPING", - return_url: params.returnUrl, - cancel_url: params.cancelUrl, - }, - }), - }) - if (!res.ok) throw new Error(`PayPal createOrder failed: ${res.status} ${await res.text().catch(() => "")}`) - const json = (await res.json()) as { id: string; links?: Array<{ rel: string; href: string }> } - return { id: json.id, approveUrl: approveUrl(json.links) } -} - -/** Capture an approved order. Returns the captured order (status COMPLETED). */ -export async function captureOrder(orderId: string): Promise<{ - status: string - custom_id?: string -} | null> { - const res = await paypalFetch(`/v2/checkout/orders/${orderId}/capture`, { - method: "POST", - body: "{}", - }) - if (!res.ok) return null - const json = (await res.json()) as { - status: string - purchase_units?: Array<{ custom_id?: string; payments?: { captures?: Array<{ custom_id?: string }> } }> - } - const unit = json.purchase_units?.[0] - const custom_id = unit?.custom_id ?? unit?.payments?.captures?.[0]?.custom_id - return { status: json.status, custom_id } -} - -export type PaypalSubscription = { - id: string - status: string - custom_id?: string - billing_info?: { next_billing_time?: string } -} - -export async function getSubscription(id: string): Promise { - const res = await paypalFetch(`/v1/billing/subscriptions/${id}`, { method: "GET" }) - if (!res.ok) return null - return (await res.json()) as PaypalSubscription -} - -export async function cancelSubscription(id: string, reason = "Cancelled by subscriber"): Promise { - const res = await paypalFetch(`/v1/billing/subscriptions/${id}/cancel`, { - method: "POST", - body: JSON.stringify({ reason }), - }) - // 204 = cancelled; 422 = already inactive (treat as success so the UI settles). - return res.ok || res.status === 204 || res.status === 422 -} diff --git a/lib/paypal/client.ts b/lib/paypal/client.ts deleted file mode 100644 index 84b3d7e..0000000 --- a/lib/paypal/client.ts +++ /dev/null @@ -1,57 +0,0 @@ -// PayPal REST API client — OAuth2 client-credentials + a thin fetch helper. -// -// Enabled only when PAYPAL_CLIENT_ID and PAYPAL_SECRET are set (mirrors the -// gating used for the other optional integrations). PAYPAL_ENVIRONMENT selects -// the sandbox (default) or live host. - -const ENVIRONMENT = process.env.PAYPAL_ENVIRONMENT === "live" ? "live" : "sandbox" - -const BASE_URL = - ENVIRONMENT === "live" ? "https://api-m.paypal.com" : "https://api-m.sandbox.paypal.com" - -export function paypalConfigured(): boolean { - return Boolean(process.env.PAYPAL_CLIENT_ID && process.env.PAYPAL_SECRET) -} - -export function paypalEnvironment() { - return ENVIRONMENT -} - -// Access tokens live ~9h; cache in-process (the app runs a persistent Node -// server, so this survives across requests) and refresh a minute early. -let cachedToken: { token: string; expiresAt: number } | null = null - -async function getAccessToken(): Promise { - if (cachedToken && cachedToken.expiresAt > Date.now() + 60_000) return cachedToken.token - - const id = process.env.PAYPAL_CLIENT_ID - const secret = process.env.PAYPAL_SECRET - if (!id || !secret) throw new Error("PayPal is not configured") - - const res = await fetch(`${BASE_URL}/v1/oauth2/token`, { - method: "POST", - headers: { - Authorization: `Basic ${Buffer.from(`${id}:${secret}`).toString("base64")}`, - "Content-Type": "application/x-www-form-urlencoded", - }, - body: "grant_type=client_credentials", - }) - if (!res.ok) throw new Error(`PayPal auth failed: ${res.status} ${await res.text().catch(() => "")}`) - - const json = (await res.json()) as { access_token: string; expires_in: number } - cachedToken = { token: json.access_token, expiresAt: Date.now() + json.expires_in * 1000 } - return cachedToken.token -} - -/** Authenticated fetch against the PayPal REST API. Path is relative (e.g. "/v1/..."). */ -export async function paypalFetch(path: string, init: RequestInit = {}): Promise { - const token = await getAccessToken() - return fetch(`${BASE_URL}${path}`, { - ...init, - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - ...(init.headers ?? {}), - }, - }) -} diff --git a/lib/paypal/fulfill.ts b/lib/paypal/fulfill.ts deleted file mode 100644 index 46d278e..0000000 --- a/lib/paypal/fulfill.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { eq } from "drizzle-orm" -import { db } from "@/lib/db" -import { profiles } from "@/lib/db/schema" -import type { Plan } from "@/types" - -// Applies PayPal subscription/order outcomes to a profile. Shared by the return -// handler (synchronous, on approval redirect) and the webhook (async, for -// renewals/cancellations). Both are idempotent. - -const RECURRING: ReadonlyArray = ["pro", "landlord"] - -export async function fulfillSubscription( - userId: string, - plan: string, - subscriptionId: string, - nextBillingTime?: string | null, - status = "active", -): Promise { - if (!RECURRING.includes(plan as Plan)) return - await db - .update(profiles) - .set({ - plan: plan as Plan, - subscription_status: status, - paypal_subscription_id: subscriptionId, - billing_provider: "paypal", - plan_expires_at: nextBillingTime ?? null, - }) - .where(eq(profiles.id, userId)) -} - -export async function fulfillLifetime(userId: string): Promise { - await db - .update(profiles) - .set({ plan: "lifetime", subscription_status: "active", billing_provider: "paypal" }) - .where(eq(profiles.id, userId)) -} - -/** Downgrade/mark a profile by its PayPal subscription id (cancel/expire/suspend). */ -export async function markPaypalSubscriptionInactive( - subscriptionId: string, - status: string, - downgrade: boolean, -): Promise { - await db - .update(profiles) - .set( - downgrade - ? { subscription_status: status, plan: "starter", paypal_subscription_id: null, plan_expires_at: null } - : { subscription_status: status }, - ) - .where(eq(profiles.paypal_subscription_id, subscriptionId)) -} diff --git a/lib/paypal/plans.ts b/lib/paypal/plans.ts deleted file mode 100644 index e37ab9d..0000000 --- a/lib/paypal/plans.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { Plan } from "@/types" - -// PayPal billing-plan IDs, one per (plan, interval). Create them once with -// `node scripts/paypal-setup-plans.mjs` and paste the printed IDs into the -// environment. A plan/interval with no configured ID simply isn't offered. -const PAYPAL_PLAN_IDS: Record = { - "pro:month": process.env.PAYPAL_PRO_MONTHLY_PLAN_ID, - "pro:year": process.env.PAYPAL_PRO_YEARLY_PLAN_ID, - "landlord:month": process.env.PAYPAL_LANDLORD_MONTHLY_PLAN_ID, - "landlord:year": process.env.PAYPAL_LANDLORD_YEARLY_PLAN_ID, -} - -/** Recurring plans PayPal can bill (lifetime is a one-time order, not a plan). */ -export const PAYPAL_RECURRING_PLANS = ["pro", "landlord"] as const - -export function getPaypalPlanId(plan: Plan, interval: "month" | "year"): string | undefined { - return PAYPAL_PLAN_IDS[`${plan}:${interval}`] || undefined -} - -/** True when at least one PayPal-billable plan is configured. */ -export function anyPaypalPlanConfigured(): boolean { - return Object.values(PAYPAL_PLAN_IDS).some(Boolean) -} - -/** Annual PayPal billing is offered only when both yearly plan IDs exist. */ -export function paypalAnnualEnabled(): boolean { - return Boolean(PAYPAL_PLAN_IDS["pro:year"] && PAYPAL_PLAN_IDS["landlord:year"]) -} diff --git a/lib/paypal/webhook.ts b/lib/paypal/webhook.ts deleted file mode 100644 index 4a7cd7e..0000000 --- a/lib/paypal/webhook.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { paypalFetch } from "./client" - -// Verify an inbound PayPal webhook using PayPal's verify-webhook-signature API. -// Requires PAYPAL_WEBHOOK_ID (from the webhook you create in the PayPal app). -// Returns false (reject) when the id is missing or verification doesn't succeed. -export async function verifyPaypalWebhook(headers: Headers, rawBody: string): Promise { - const webhookId = process.env.PAYPAL_WEBHOOK_ID - if (!webhookId) return false - - let event: unknown - try { - event = JSON.parse(rawBody) - } catch { - return false - } - - try { - const res = await paypalFetch("/v1/notifications/verify-webhook-signature", { - method: "POST", - body: JSON.stringify({ - auth_algo: headers.get("paypal-auth-algo"), - cert_url: headers.get("paypal-cert-url"), - transmission_id: headers.get("paypal-transmission-id"), - transmission_sig: headers.get("paypal-transmission-sig"), - transmission_time: headers.get("paypal-transmission-time"), - webhook_id: webhookId, - webhook_event: event, - }), - }) - if (!res.ok) return false - const json = (await res.json()) as { verification_status?: string } - return json.verification_status === "SUCCESS" - } catch { - return false - } -} diff --git a/lib/storage.ts b/lib/storage.ts index 8fe5ec7..b3819a6 100644 --- a/lib/storage.ts +++ b/lib/storage.ts @@ -143,6 +143,53 @@ function sanitizeSegment(s: string): string { return s.replace(/[^a-zA-Z0-9_-]/g, "_") } +/** + * True iff a storage key lives in the given owner's namespace (`/…`). + * Keys are generated server-side as `//`, so any + * client-supplied key/path whose first segment differs belongs to another tenant + * (or is malformed) and must be rejected. + */ +export function keyBelongsToOwner(key: string | null | undefined, ownerId: string): boolean { + if (!key || !ownerId) return false + const first = key.replace(/^\/+/, "").split(/[\\/]+/)[0] + return first === sanitizeSegment(ownerId) +} + +/** + * Lightweight magic-byte check: reject a file whose real content doesn't match + * its claimed extension (e.g. an HTML/script payload renamed to `.pdf`). Types + * without a reliable file signature (csv/txt) are allowed through. `head` should + * be the first ~16 bytes of the file. + */ +export function contentMatchesExtension(head: Buffer, ext: string): boolean { + const at = (offset: number, sig: number[]) => + head.length >= offset + sig.length && sig.every((b, i) => head[offset + i] === b) + switch (ext) { + case "pdf": + return at(0, [0x25, 0x50, 0x44, 0x46]) // %PDF + case "png": + return at(0, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]) + case "jpg": + case "jpeg": + return at(0, [0xff, 0xd8, 0xff]) + case "gif": + return at(0, [0x47, 0x49, 0x46, 0x38]) // GIF8 + case "webp": + return at(0, [0x52, 0x49, 0x46, 0x46]) && at(8, [0x57, 0x45, 0x42, 0x50]) // RIFF…WEBP + case "docx": + case "xlsx": + return at(0, [0x50, 0x4b, 0x03, 0x04]) || at(0, [0x50, 0x4b, 0x05, 0x06]) // zip (PK) + case "doc": + case "xls": + return at(0, [0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1]) || at(0, [0x50, 0x4b]) // OLE or zip + case "csv": + case "txt": + return true // no reliable signature + default: + return true + } +} + async function bodyToBuffer(body: GetObjectCommandOutput["Body"]): Promise { if (!body) return Buffer.alloc(0) // The AWS SDK v3 Node runtime adds transformToByteArray() to the stream body. @@ -194,6 +241,36 @@ export async function saveFile( return { key, size: file.size, type } } +/** + * Persist raw bytes under `${userId}/${scope}/.` (server-generated, + * so the key is always in the owner's namespace) and return the storage key. + * Used for server-side artifacts like signed e-sign PDFs. + */ +export async function saveBuffer( + buffer: Buffer, + opts: { userId: string; scope: string; ext: string } +): Promise<{ key: string }> { + const ext = opts.ext.replace(/[^a-z0-9]/gi, "").toLowerCase() || "bin" + const key = `${sanitizeSegment(opts.userId)}/${sanitizeSegment(opts.scope)}/${Date.now()}-${randomBytes(6).toString("hex")}.${ext}` + if (usingSpaces()) { + await s3().send( + new PutObjectCommand({ + Bucket: SPACES_BUCKET, + Key: key, + Body: buffer, + ContentType: contentTypeForKey(key), + ACL: "private", + }) + ) + } else { + if (process.env.NODE_ENV === "production") throw new StorageNotConfiguredError() + const abs = resolveKey(key) + await fs.mkdir(path.dirname(abs), { recursive: true }) + await fs.writeFile(abs, buffer) + } + return { key } +} + export async function readFile(key: string): Promise { if (usingSpaces()) { const res = await s3().send(new GetObjectCommand({ Bucket: SPACES_BUCKET, Key: assertSafeKey(key) })) @@ -227,7 +304,10 @@ export async function presignGetUrl( return toCdnUrl(signed) } -export async function deleteFile(key: string): Promise { +export async function deleteFile(key: string, ownerId: string): Promise { + // Defense in depth: never delete an object outside the caller's own namespace, + // even if a stored storage_path was tampered with to point at another tenant. + if (!keyBelongsToOwner(key, ownerId)) return try { if (usingSpaces()) { await s3().send(new DeleteObjectCommand({ Bucket: SPACES_BUCKET, Key: assertSafeKey(key) })) diff --git a/next.config.ts b/next.config.ts index 6bf5e39..aa966e7 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,4 +1,5 @@ import type { NextConfig } from "next"; +import { withSentryConfig } from "@sentry/nextjs"; // NOTE: The Content-Security-Policy is set per-request in `proxy.ts` (Next // middleware) so `script-src` can carry a per-request nonce instead of @@ -31,4 +32,16 @@ const nextConfig: NextConfig = { }, }; -export default nextConfig; +export default withSentryConfig(nextConfig, { + org: "phluit", + project: "property-management-network", + // Only print source-map upload logs in CI. + silent: !process.env.CI, + // Upload a wider set of client source maps for readable stack traces. + widenClientFileUpload: true, + // Tree-shake Sentry's debug logger to shrink the client bundle. + disableLogger: true, + // Source-map upload runs at build time only when SENTRY_AUTH_TOKEN is set; + // without it the build still succeeds (stack traces just aren't un-minified). +}); + diff --git a/package-lock.json b/package-lock.json index e03907f..25b707d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,9 @@ "": { "name": "property-management-network", "version": "0.1.0", + "license": "UNLICENSED", "dependencies": { + "@anthropic-ai/sdk": "^0.110.0", "@aws-sdk/client-s3": "^3.1077.0", "@aws-sdk/s3-request-presigner": "^3.1078.0", "@fullcalendar/daygrid": "^6.1.21", @@ -16,6 +18,7 @@ "@fullcalendar/react": "^6.1.21", "@fullcalendar/timegrid": "^6.1.21", "@radix-ui/react-switch": "^1.2.6", + "@sentry/nextjs": "^10.63.0", "@types/papaparse": "^5.5.2", "better-auth": "^1.6.20", "clsx": "^2.1.1", @@ -68,6 +71,66 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.110.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.110.0.tgz", + "integrity": "sha512-hOP4bNYXDFHDxxiEgzlILXrxZIYCDnhe8sry0RDRKD/QnsEpvZcQpablCdm9X/WuD/YgOiSIkkqsL1mLLlTqJw==", + "dependencies": { + "json-schema-to-ts": "^3.1.1", + "standardwebhooks": "^1.0.0" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@apm-js-collab/code-transformer": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@apm-js-collab/code-transformer/-/code-transformer-0.15.0.tgz", + "integrity": "sha512-XmXYVs8CzJ1Aj79noVbn2weUO/XWtRyURpGqx7aU7DOXlUQhR0WKOQNF0okh7PCeY37vxf7kU3v57OAkEPm3ww==", + "dependencies": { + "@types/estree": "^1.0.8", + "astring": "^1.9.0", + "esquery": "^1.7.0", + "meriyah": "^6.1.4", + "semifies": "^1.0.0", + "source-map": "^0.6.0" + }, + "bin": { + "code-transformer": "cli.js" + } + }, + "node_modules/@apm-js-collab/code-transformer-bundler-plugins": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@apm-js-collab/code-transformer-bundler-plugins/-/code-transformer-bundler-plugins-0.5.0.tgz", + "integrity": "sha512-YxLBY5nGlurL7QeJLq6e5g0ouBpAp0pwgyA/5rHXEXwhiPLn9ZHbT+Y2LlP90GT872cSocfjWRYu/fnpuBudNQ==", + "dependencies": { + "@apm-js-collab/code-transformer": "^0.15.0", + "es-module-lexer": "^2.1.0", + "magic-string": "^0.30.21", + "module-details-from-path": "^1.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@apm-js-collab/tracing-hooks": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@apm-js-collab/tracing-hooks/-/tracing-hooks-0.10.1.tgz", + "integrity": "sha512-w2OWXR7FWrKqSziuE9+QclaZrStxO/8+OwbXM635s/zs0Eez1Qo3ivSPdB2WsaPY/iznKTytONPx/PitD7IXcA==", + "dependencies": { + "@apm-js-collab/code-transformer": "^0.15.0", + "debug": "^4.4.1", + "module-details-from-path": "^1.0.4" + } + }, "node_modules/@aws-sdk/checksums": { "version": "3.1000.10", "resolved": "https://registry.npmjs.org/@aws-sdk/checksums/-/checksums-3.1000.10.tgz", @@ -378,7 +441,6 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", @@ -393,7 +455,6 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -403,7 +464,6 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.0", @@ -434,7 +494,6 @@ "version": "7.29.1", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.29.0", @@ -451,7 +510,6 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.28.6", @@ -468,7 +526,6 @@ "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -478,7 +535,6 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.28.6", @@ -492,7 +548,6 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.28.6", @@ -510,7 +565,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -520,7 +574,6 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -530,7 +583,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -540,7 +592,6 @@ "version": "7.29.2", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.28.6", @@ -554,7 +605,6 @@ "version": "7.29.2", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.29.0" @@ -579,7 +629,6 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.28.6", @@ -594,7 +643,6 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.0", @@ -613,7 +661,6 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -2333,7 +2380,6 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -2344,7 +2390,6 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -2355,24 +2400,31 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" } }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -2606,6 +2658,86 @@ "node": ">=12.4.0" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.214.0.tgz", + "integrity": "sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA==", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", + "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.214.0.tgz", + "integrity": "sha512-MHqEX5Dk59cqVah5LiARMACku7jXSVk9iVDWOea4x3cr7VfdByeDCURK6o1lntT1JS/Tsovw01UJrBhN3/uC5w==", + "dependencies": { + "@opentelemetry/api-logs": "0.214.0", + "import-in-the-middle": "^3.0.0", + "require-in-the-middle": "^8.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", + "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", + "dependencies": { + "@opentelemetry/core": "2.8.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.8.0.tgz", + "integrity": "sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ==", + "dependencies": { + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/semantic-conventions": { "version": "1.41.1", "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz", @@ -2857,6 +2989,390 @@ "url": "https://opencollective.com/immer" } }, + "node_modules/@rollup/plugin-commonjs": { + "version": "28.0.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.1.tgz", + "integrity": "sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA==", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.2.0", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=16.0.0 || 14 >= 14.17" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -2864,6 +3380,433 @@ "dev": true, "license": "MIT" }, + "node_modules/@sentry/babel-plugin-component-annotate": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-5.3.0.tgz", + "integrity": "sha512-p4q8gn8wcFqZGP/s2MnJCAAd8fTikaU6A0mM97RDHQgStcrYiaS0Sc5zUNfb1V+UOLPuvdEdL6MwyxfzjYJQTA==", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@sentry/browser": { + "version": "10.63.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.63.0.tgz", + "integrity": "sha512-0mi56YOkwgyjdLOcN5cB1//EcYzEOt3NZ2GLygE92B3zAAwVM1WgbmibZCXToKFClH7z1uH3VWVfBffmkwIMYw==", + "dependencies": { + "@sentry/browser-utils": "10.63.0", + "@sentry/core": "10.63.0", + "@sentry/feedback": "10.63.0", + "@sentry/replay": "10.63.0", + "@sentry/replay-canvas": "10.63.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/browser-utils": { + "version": "10.63.0", + "resolved": "https://registry.npmjs.org/@sentry/browser-utils/-/browser-utils-10.63.0.tgz", + "integrity": "sha512-DhUGNN+CH8fzAs6qAsueKPU70qShyTX3NxLhIP+l5DbGXDSXpYXBT6s8ubZus0/LhxpLvI0iSyNIDvZRD/gZaA==", + "dependencies": { + "@sentry/core": "10.63.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/bundler-plugin-core": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-5.3.0.tgz", + "integrity": "sha512-L5T60sWdAI3qWwdg3Ptwek/0TY59PERrxyqp4XMUkroayQvGd9r5dIW9Q1kSeXX9iJ442nXbFZKAOyCKV4Z13Q==", + "dependencies": { + "@babel/core": "^7.18.5", + "@sentry/babel-plugin-component-annotate": "5.3.0", + "@sentry/cli": "^2.58.5", + "dotenv": "^16.3.1", + "find-up": "^5.0.0", + "glob": "^13.0.6", + "magic-string": "~0.30.8" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@sentry/bundler-plugin-core/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/@sentry/cli": { + "version": "2.58.6", + "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.58.6.tgz", + "integrity": "sha512-baBcNPLLfUi9WuL+Tpri9BFaAdvugZIKelC5X0tt0Zdy+K0K+PCVSrnNmwMWU/HyaF/SEv6b6UHnXIdqanBlcg==", + "hasInstallScript": true, + "dependencies": { + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.7", + "progress": "^2.0.3", + "proxy-from-env": "^1.1.0", + "which": "^2.0.2" + }, + "bin": { + "sentry-cli": "bin/sentry-cli" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@sentry/cli-darwin": "2.58.6", + "@sentry/cli-linux-arm": "2.58.6", + "@sentry/cli-linux-arm64": "2.58.6", + "@sentry/cli-linux-i686": "2.58.6", + "@sentry/cli-linux-x64": "2.58.6", + "@sentry/cli-win32-arm64": "2.58.6", + "@sentry/cli-win32-i686": "2.58.6", + "@sentry/cli-win32-x64": "2.58.6" + } + }, + "node_modules/@sentry/cli-darwin": { + "version": "2.58.6", + "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.58.6.tgz", + "integrity": "sha512-udAVvcyfNa0R+95GvPz/+43/N3TC0TYKdkQ7D7jhPSzbcMc7l2fxRNN5yB3UpCA5fWFnW4toeaqwDBhb/Wh3LA==", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-arm": { + "version": "2.58.6", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.58.6.tgz", + "integrity": "sha512-pD0LAt5PcUzAinBwvDqc66x9+2CabHEv486yP0gRjWO7SakbaxmfVq/EXd8VLq/Tzi39LAu422UYK1lpW3MILw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux", + "freebsd", + "android" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-arm64": { + "version": "2.58.6", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.58.6.tgz", + "integrity": "sha512-q8mEcNNmeXMy5i+jWT30TVpH7LcP4HD21CD5XRSPAd/a912HF6EpK0ybf/1USO14WOhoXbAGi9txwaWabSe33g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux", + "freebsd", + "android" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-i686": { + "version": "2.58.6", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.58.6.tgz", + "integrity": "sha512-q8vNJi1eOV/4vxAFWBsEwLHoSYapaZHIf4j76KJGJXFKTkEbsjCOOsKbwUIBTQQhRgV4DFWh3ryfsPS/que4Kg==", + "cpu": [ + "x86", + "ia32" + ], + "optional": true, + "os": [ + "linux", + "freebsd", + "android" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-x64": { + "version": "2.58.6", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.58.6.tgz", + "integrity": "sha512-DZu956Mhi3ZRjTBe1WdbGV46ldVbA8d2rgp/fh51GsI25zjBHah4wZnPTSzpc+YqxU6pJpg579B/r3jrIK530Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux", + "freebsd", + "android" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-win32-arm64": { + "version": "2.58.6", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.58.6.tgz", + "integrity": "sha512-nj0Ff/kmAB73EPDhR8B4O9r+NUHK5GkPCkGWC+kXVemqAJWL5jcJ5KdxG0l/S0z6RoEoltID8/43/B+TaMlT7A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-win32-i686": { + "version": "2.58.6", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.58.6.tgz", + "integrity": "sha512-WNZiDzPbgsEMQWq4avsQ391v/xWKJDIWWWo9GYl+N/w5qcYKkoDW7wQG7T9FasI6ENn68phChTOAPXXxbfAdOg==", + "cpu": [ + "x86", + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-win32-x64": { + "version": "2.58.6", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.58.6.tgz", + "integrity": "sha512-R35WJ17oF4D2eqI1DR2sQQqr0fjRTt5xoP16WrTu91XM2lndRMFsnjh+/GttbxapLCBNlrjzia99MJ0PZHZpgA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/conventions": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@sentry/conventions/-/conventions-0.12.0.tgz", + "integrity": "sha512-z1JQrl/1SLY+8wpzvork6vl+fpsg/oCCxM7HWWhUnI/R+OGNyoIzieQuggX3uUMY7NBtp8UWCQx6FeFazzOF9g==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@sentry/core": { + "version": "10.63.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.63.0.tgz", + "integrity": "sha512-OtUbsrnbEHffOF2S2+M5zXa3HIM0U2b4CDVLKMY1dgS0J3ivRF8XvkjvyIcEG/y8JXnwXbnprLyjhG+AqMdUZQ==", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/feedback": { + "version": "10.63.0", + "resolved": "https://registry.npmjs.org/@sentry/feedback/-/feedback-10.63.0.tgz", + "integrity": "sha512-If/+72xFg9ylz4twUo3U9gUpZ+Ys+T/3Y09WH7r2gGhWEOF9bp+ta94+Pg7Lb0M2nVD7waz4OxIvB49GEvtLDA==", + "dependencies": { + "@sentry/core": "10.63.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/nextjs": { + "version": "10.63.0", + "resolved": "https://registry.npmjs.org/@sentry/nextjs/-/nextjs-10.63.0.tgz", + "integrity": "sha512-SN3tBm+wpDmr4GONaxuIRjQglAiPBKF7JEsQlli1HNfar1JG+fRsxWy0/aKcn9Kp/XX1kOpFgW9tcfuE4UKm7Q==", + "dependencies": { + "@opentelemetry/api": "^1.9.1", + "@rollup/plugin-commonjs": "28.0.1", + "@sentry/browser-utils": "10.63.0", + "@sentry/bundler-plugin-core": "^5.3.0", + "@sentry/conventions": "^0.12.0", + "@sentry/core": "10.63.0", + "@sentry/node": "10.63.0", + "@sentry/opentelemetry": "10.63.0", + "@sentry/react": "10.63.0", + "@sentry/vercel-edge": "10.63.0", + "@sentry/webpack-plugin": "^5.3.0", + "rollup": "^4.60.3", + "stacktrace-parser": "^0.1.11" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "next": "^13.2.0 || ^14.0 || ^15.0.0-rc.0 || ^16.0.0-0" + } + }, + "node_modules/@sentry/node": { + "version": "10.63.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-10.63.0.tgz", + "integrity": "sha512-E+JfDTdUDGQPRsAfCTR2YgmQgxYdoxk4ks6niHN+ByW8alEZL+nXlcN9vI57qj1LsS4v2jjfLxJf1/cMMt84YA==", + "dependencies": { + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/instrumentation": "^0.214.0", + "@opentelemetry/sdk-trace-base": "^2.6.1", + "@opentelemetry/semantic-conventions": "^1.40.0", + "@sentry/conventions": "^0.12.0", + "@sentry/core": "10.63.0", + "@sentry/node-core": "10.63.0", + "@sentry/opentelemetry": "10.63.0", + "@sentry/server-utils": "10.63.0", + "import-in-the-middle": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node-core": { + "version": "10.63.0", + "resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-10.63.0.tgz", + "integrity": "sha512-TaNtkGDRNxH3SjOea2PDtaebkNjMbAH8ZFsEcwlqmadpS7nqSR7z6slZy/iu7y1nLiUdbmcM5JmXwxksy52WRQ==", + "dependencies": { + "@sentry/conventions": "^0.12.0", + "@sentry/core": "10.63.0", + "@sentry/opentelemetry": "10.63.0", + "import-in-the-middle": "^3.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "^1.30.1 || ^2.1.0", + "@opentelemetry/exporter-trace-otlp-http": ">=0.57.0 <1", + "@opentelemetry/instrumentation": ">=0.57.1 <1", + "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@opentelemetry/core": { + "optional": true + }, + "@opentelemetry/exporter-trace-otlp-http": { + "optional": true + }, + "@opentelemetry/instrumentation": { + "optional": true + }, + "@opentelemetry/sdk-trace-base": { + "optional": true + } + } + }, + "node_modules/@sentry/opentelemetry": { + "version": "10.63.0", + "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-10.63.0.tgz", + "integrity": "sha512-8yqi8+Ej/anmMn82blXA0BNMeAMs4av6nx0DzhxDrFya28ZaYOn19PChd3erMidfU0HnLLFNqWiFlYxBKq+/KA==", + "dependencies": { + "@sentry/conventions": "^0.12.0", + "@sentry/core": "10.63.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "^1.30.1 || ^2.1.0", + "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0" + } + }, + "node_modules/@sentry/react": { + "version": "10.63.0", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-10.63.0.tgz", + "integrity": "sha512-+/Y0dd4EMqyqYBJ1D3bAYYuG+ccIx5+IFcbTZ9p+XWnW6nNIVjy5zttVftYo6xOmtTQbzRuPT/vO4dqDHKKmfw==", + "dependencies": { + "@sentry/browser": "10.63.0", + "@sentry/core": "10.63.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.14.0 || 17.x || 18.x || 19.x" + } + }, + "node_modules/@sentry/replay": { + "version": "10.63.0", + "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-10.63.0.tgz", + "integrity": "sha512-u4fDaLbd4QmJbU0qGzV5g2B2hjw5utdeZzpTrmq565AS5o6mfaZdCz30zF9R2Unkn0g9SJr90piTN2RMwvDrkw==", + "dependencies": { + "@sentry/browser-utils": "10.63.0", + "@sentry/core": "10.63.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/replay-canvas": { + "version": "10.63.0", + "resolved": "https://registry.npmjs.org/@sentry/replay-canvas/-/replay-canvas-10.63.0.tgz", + "integrity": "sha512-1Dg6yo+KDNZcE9M6V2EP4DGgTDJMcUgg5ui69w/E96ZZPWErS/bibK2bGj20H3qwpJXlnEwXB5YAJ2fZ620T1A==", + "dependencies": { + "@sentry/core": "10.63.0", + "@sentry/replay": "10.63.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/server-utils": { + "version": "10.63.0", + "resolved": "https://registry.npmjs.org/@sentry/server-utils/-/server-utils-10.63.0.tgz", + "integrity": "sha512-7NN//DG9Yak8t2+6WiEcNmN269iHRVdtZtZIwucEd0OXyZ3FEBBDaBF+bT9V6H/kPtUvVMkHQ72Bn2Xs5JYGxg==", + "dependencies": { + "@apm-js-collab/code-transformer": "^0.15.0", + "@apm-js-collab/code-transformer-bundler-plugins": "^0.5.0", + "@apm-js-collab/tracing-hooks": "^0.10.0", + "@sentry/conventions": "^0.12.0", + "@sentry/core": "10.63.0", + "magic-string": "~0.30.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/vercel-edge": { + "version": "10.63.0", + "resolved": "https://registry.npmjs.org/@sentry/vercel-edge/-/vercel-edge-10.63.0.tgz", + "integrity": "sha512-6vay7/Skgjt1SiDchobaN7mOeSDRwhQUikVvuT7Q/0nX5Xg5TRlSMbqwcllqKxwDXdJiW3MVdqqLixY1c8WjJA==", + "dependencies": { + "@opentelemetry/api": "^1.9.1", + "@sentry/core": "10.63.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/webpack-plugin": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@sentry/webpack-plugin/-/webpack-plugin-5.3.0.tgz", + "integrity": "sha512-i3OQUrS0FZlXLgq57RIKDp+vHHzuvYKPCKewAPXULWKMsBXFGhP6veGRQ+6To/pmZkkXjEX5ofVNDy9C3jEPKQ==", + "dependencies": { + "@sentry/bundler-plugin-core": "5.3.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "webpack": ">=5.0.0" + } + }, "node_modules/@smithy/core": { "version": "3.29.0", "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.29.0.tgz", @@ -2939,6 +3882,11 @@ "node": ">=18.0.0" } }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==" + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -3306,11 +4254,9 @@ "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==" }, "node_modules/@types/geojson": { "version": "7946.0.16", @@ -3322,7 +4268,6 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, "license": "MIT" }, "node_modules/@types/json5": { @@ -3989,11 +4934,168 @@ "win32" ] }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "peer": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "peer": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "peer": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "peer": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "peer": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "peer": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "peer": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "peer": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "peer": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "peer": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "peer": true + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -4002,6 +5104,26 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "peer": true, + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -4012,6 +5134,17 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", @@ -4029,6 +5162,45 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "peer": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "peer": true + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -4229,6 +5401,14 @@ "dev": true, "license": "MIT" }, + "node_modules/astring": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "bin": { + "astring": "bin/astring" + } + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -4460,7 +5640,6 @@ "version": "4.28.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -4493,8 +5672,7 @@ "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "devOptional": true + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, "node_modules/call-bind": { "version": "1.0.8", @@ -4613,6 +5791,20 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "peer": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==" + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -4648,6 +5840,17 @@ "dev": true, "license": "MIT" }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "peer": true + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -4659,7 +5862,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, "license": "MIT" }, "node_modules/core-js": { @@ -4902,7 +6104,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -5172,7 +6373,6 @@ "version": "1.5.331", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.331.tgz", "integrity": "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==", - "dev": true, "license": "ISC" }, "node_modules/emoji-regex": { @@ -5183,14 +6383,12 @@ "license": "MIT" }, "node_modules/enhanced-resolve": { - "version": "5.20.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", - "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", - "dev": true, - "license": "MIT", + "version": "5.24.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.1.tgz", + "integrity": "sha512-7DdUaTjmNwMcH2gLr1qycesKII3BK4RLy/mdAb7x10Lq7bR4aNKHt1BR1ZALSv0rPM/hF5wYF0PhGop/rJm8vw==", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.3.0" + "tapable": "^2.3.3" }, "engines": { "node": ">=10.13.0" @@ -5314,6 +6512,11 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.0.tgz", + "integrity": "sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -5429,7 +6632,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5812,7 +7014,6 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" @@ -5825,7 +7026,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" @@ -5838,12 +7038,16 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -5860,11 +7064,19 @@ "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "license": "MIT" }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "peer": true, + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -5928,6 +7140,27 @@ "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", "license": "(MIT AND Zlib)" }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==" + }, + "node_modules/fast-uri": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", + "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "peer": true + }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -5974,7 +7207,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, "license": "MIT", "dependencies": { "locate-path": "^6.0.0", @@ -6055,7 +7287,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -6121,7 +7352,6 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -6197,6 +7427,22 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -6210,6 +7456,39 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -6257,7 +7536,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/has-bigints": { @@ -6277,7 +7555,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6385,6 +7662,18 @@ "node": ">=8.0.0" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -6422,6 +7711,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-in-the-middle": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", + "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "dependencies": { + "acorn": "^8.15.0", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^2.2.0", + "module-details-from-path": "^1.0.4" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -6732,6 +8035,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dependencies": { + "@types/estree": "*" + } + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -6888,7 +8199,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/iterator.prototype": { @@ -6909,6 +8219,35 @@ "node": ">= 0.4" } }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "peer": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -6931,7 +8270,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -6951,7 +8289,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -6967,6 +8304,18 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -6985,7 +8334,6 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -7345,11 +8693,23 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/loader-runner": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", + "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", + "peer": true, + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, "license": "MIT", "dependencies": { "p-locate": "^5.0.0" @@ -7385,7 +8745,6 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, "license": "ISC", "dependencies": { "yallist": "^3.0.2" @@ -7404,7 +8763,6 @@ "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" @@ -7420,6 +8778,12 @@ "node": ">= 0.4" } }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "peer": true + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -7430,6 +8794,14 @@ "node": ">= 8" } }, + "node_modules/meriyah": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/meriyah/-/meriyah-6.1.4.tgz", + "integrity": "sha512-Sz8FzjzI0kN13GK/6MVEsVzMZEPvOhnmmI1lU5+/1cGOiK3QUahntrNNtdVeihrO7t9JpoH75iMNXg6R6uWflQ==", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -7444,6 +8816,15 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -7467,6 +8848,79 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minimizer-webpack-plugin": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/minimizer-webpack-plugin/-/minimizer-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-DoeAZz8Q1C1znwsUzej1fdoi4jCf7/+Em27ouLqfK/+3m8G+D7yDhUwrc3CNhjSzGUN1kn7Iv4sWmjflQHenpw==", + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/module-details-from-path": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==" + }, "node_modules/motion-dom": { "version": "12.38.0", "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz", @@ -7486,7 +8940,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -7544,6 +8997,12 @@ "dev": true, "license": "MIT" }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "peer": true + }, "node_modules/next": { "version": "16.2.2", "resolved": "https://registry.npmjs.org/next/-/next-16.2.2.tgz", @@ -7644,11 +9103,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-releases": { "version": "2.0.37", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", - "dev": true, "license": "MIT" }, "node_modules/nodemailer": { @@ -7843,7 +9320,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" @@ -7859,7 +9335,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, "license": "MIT", "dependencies": { "p-limit": "^3.0.2" @@ -7894,7 +9369,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7917,6 +9391,29 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -8157,6 +9654,14 @@ "node": ">= 0.8.0" } }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -8169,6 +9674,11 @@ "react-is": "^16.13.1" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -8356,6 +9866,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-in-the-middle": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", + "integrity": "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==", + "dependencies": { + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3" + }, + "engines": { + "node": ">=9.3.0 || >=8.10.0 <9.0.0" + } + }, "node_modules/reselect": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", @@ -8427,6 +9958,49 @@ "node": ">= 0.8.15" } }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, "node_modules/rou3": { "version": "0.7.12", "resolved": "https://registry.npmjs.org/rou3/-/rou3-0.7.12.tgz", @@ -8517,11 +10091,68 @@ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "peer": true + }, + "node_modules/semifies": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semifies/-/semifies-1.0.0.tgz", + "integrity": "sha512-xXR3KGeoxTNWPD4aBvL5NUpMTT7WMANr3EWnaS190QVkY52lqqcVRD7Q05UVbBhiWDGWMlJEUam9m7uFFGVScw==" + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -8752,7 +10383,6 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -8770,7 +10400,6 @@ "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "devOptional": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -8802,6 +10431,26 @@ "node": ">=0.1.14" } }, + "node_modules/stacktrace-parser": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz", + "integrity": "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==", + "dependencies": { + "type-fest": "^0.7.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -9046,11 +10695,9 @@ "license": "MIT" }, "node_modules/tapable": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", - "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", - "dev": true, - "license": "MIT", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", "engines": { "node": ">=6" }, @@ -9059,6 +10706,24 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/terser": { + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz", + "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==", + "peer": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/text-segmentation": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", @@ -9136,6 +10801,16 @@ "node": ">=8.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==" + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -9683,6 +11358,14 @@ "node": ">= 0.8.0" } }, + "node_modules/type-fest": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", + "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", + "engines": { + "node": ">=8" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -9863,7 +11546,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, "funding": [ { "type": "opencollective", @@ -9941,11 +11623,112 @@ "d3-timer": "^3.0.1" } }, + "node_modules/watchpack": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", + "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", + "peer": true, + "dependencies": { + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/webpack": { + "version": "5.108.3", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.108.3.tgz", + "integrity": "sha512-hOpaCHmQVVY66IVTjofnH14IgSdmod2aquSGHGuYig/OIdWge01Hk2Wt988DZcwXumFUT4+FvJY5N+ikl8o/ww==", + "peer": true, + "dependencies": { + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.22.2", + "es-module-lexer": "^2.1.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "graceful-fs": "^4.2.11", + "loader-runner": "^4.3.2", + "mime-db": "^1.54.0", + "minimizer-webpack-plugin": "^5.6.1", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "watchpack": "^2.5.2", + "webpack-sources": "^3.5.0" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.0.tgz", + "integrity": "sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==", + "peer": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "peer": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -10092,14 +11875,12 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, "license": "ISC" }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" diff --git a/package.json b/package.json index 03bba16..57701f7 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "db:studio": "drizzle-kit studio" }, "dependencies": { + "@anthropic-ai/sdk": "^0.110.0", "@aws-sdk/client-s3": "^3.1077.0", "@aws-sdk/s3-request-presigner": "^3.1078.0", "@fullcalendar/daygrid": "^6.1.21", @@ -22,6 +23,7 @@ "@fullcalendar/react": "^6.1.21", "@fullcalendar/timegrid": "^6.1.21", "@radix-ui/react-switch": "^1.2.6", + "@sentry/nextjs": "^10.63.0", "@types/papaparse": "^5.5.2", "better-auth": "^1.6.20", "clsx": "^2.1.1", diff --git a/proxy.ts b/proxy.ts index 760a958..325004c 100644 --- a/proxy.ts +++ b/proxy.ts @@ -28,6 +28,19 @@ const PROTECTED_PATHS = [ const AUTH_PATHS = ["/login", "/signup", "/forgot-password"] +// Origin of the Sentry ingest endpoint, derived from the public DSN so the +// CSP stays in sync with whatever project/region the DSN points at. Returns +// null when Sentry is not configured. +function sentryIngestOrigin(): string | null { + const dsn = process.env.NEXT_PUBLIC_SENTRY_DSN + if (!dsn) return null + try { + return new URL(dsn).origin + } catch { + return null + } +} + // Build the per-request Content-Security-Policy. `script-src` carries a // per-request nonce instead of 'unsafe-inline'. `style-src` keeps // 'unsafe-inline' because Radix / Tailwind / framer-motion inject inline @@ -36,6 +49,7 @@ const AUTH_PATHS = ["/login", "/signup", "/forgot-password"] // scripts automatically. function buildCsp(nonce: string): string { const isDev = process.env.NODE_ENV !== "production" + const sentry = sentryIngestOrigin() // In development, Next.js/React and Turbopack HMR require eval() for hot // reloading and debugging features, and open a dev websocket. These are NOT @@ -43,9 +57,14 @@ function buildCsp(nonce: string): string { const scriptSrc = isDev ? `script-src 'self' 'nonce-${nonce}' 'unsafe-eval' https://challenges.cloudflare.com` : `script-src 'self' 'nonce-${nonce}' https://challenges.cloudflare.com` - const connectSrc = isDev - ? "connect-src 'self' ws: wss: https://api.stripe.com https://api.openai.com https://challenges.cloudflare.com" - : "connect-src 'self' https://api.stripe.com https://api.openai.com https://challenges.cloudflare.com" + const connectSrc = [ + "connect-src 'self'", + isDev ? "ws: wss:" : "", + "https://api.stripe.com https://api.openai.com https://challenges.cloudflare.com", + sentry ?? "", + ] + .filter(Boolean) + .join(" ") return [ "default-src 'self'", @@ -54,13 +73,39 @@ function buildCsp(nonce: string): string { scriptSrc, "font-src 'self' data:", connectSrc, + // Sentry Session Replay spins up its compression worker from a blob: URL; + // without worker-src the browser falls back to script-src and blocks it. + "worker-src 'self' blob:", "frame-src https://js.stripe.com https://hooks.stripe.com https://challenges.cloudflare.com", "frame-ancestors 'none'", "base-uri 'self'", "form-action 'self'", + "object-src 'none'", ].join("; ") } +// Cookie presence is only a hint (cheap, no DB). Before bouncing a visitor off +// an auth page we confirm the session is actually alive — otherwise a stale +// cookie loops forever: /dashboard → /login (server sees no session) → +// /dashboard (proxy sees a cookie) → … until ERR_TOO_MANY_REDIRECTS. +// "unknown" (auth service unreachable / rate-limited) renders the auth page +// without touching cookies, which is safe in both directions. +async function sessionState(request: NextRequest): Promise<"valid" | "invalid" | "unknown"> { + try { + const base = process.env.BETTER_AUTH_URL ?? request.nextUrl.origin + const res = await fetch(new URL("/api/auth/get-session", base), { + headers: { cookie: request.headers.get("cookie") ?? "" }, + cache: "no-store", + }) + if (!res.ok) return "unknown" + // Better Auth returns JSON `null` when the session is missing or revoked. + const session = await res.json() + return session ? "valid" : "invalid" + } catch { + return "unknown" + } +} + export async function proxy(request: NextRequest) { const pathname = request.nextUrl.pathname @@ -76,10 +121,18 @@ export async function proxy(request: NextRequest) { } const isAuthPage = AUTH_PATHS.some((p) => pathname.startsWith(p)) + let dropStaleSessionCookie = false if (isAuthPage && sessionCookie) { - const url = request.nextUrl.clone() - url.pathname = "/dashboard" - return NextResponse.redirect(url) + const state = await sessionState(request) + if (state === "valid") { + const url = request.nextUrl.clone() + url.pathname = "/dashboard" + return NextResponse.redirect(url) + } + // Dead cookie (session revoked or expired): render the auth page and drop + // the cookie below so protected paths stop treating this visitor as + // signed in. On "unknown", render the page but keep the cookie. + dropStaleSessionCookie = state === "invalid" } // Per-request CSP nonce. UUID contains only hex + dashes, so it never @@ -96,6 +149,21 @@ export async function proxy(request: NextRequest) { const response = NextResponse.next({ request: { headers: requestHeaders } }) // Also set the CSP on the outgoing response so the browser enforces it. response.headers.set("Content-Security-Policy", csp) + + if (dropStaleSessionCookie) { + // Covers both the plain and __Secure-prefixed Better Auth cookie names. + for (const cookie of request.cookies.getAll()) { + if (!cookie.name.includes("better-auth.session_token")) continue + response.cookies.set(cookie.name, "", { + maxAge: 0, + path: "/", + httpOnly: true, + sameSite: "lax", + secure: cookie.name.startsWith("__Secure-"), + }) + } + } + return response } diff --git a/public/file.svg b/public/file.svg deleted file mode 100644 index 004145c..0000000 --- a/public/file.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/globe.svg b/public/globe.svg deleted file mode 100644 index 567f17b..0000000 --- a/public/globe.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/icon-192.png b/public/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..95083be906faee714aa963c8dcfc4b7c74dad38b GIT binary patch literal 23402 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 ReN+Gd002ovPDHLkV1gKal+OSF literal 0 HcmV?d00001 diff --git a/public/icon-512.png b/public/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..a31bbb1ceac77a73d182b7141d3856c3b98bfbc3 GIT binary patch literal 139564 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 QT!Eh2Y37v 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 GDdIFT#T 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ⅅL3sqykrW3L14CzQDIT{ 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 bY+d;O4<;lIp$mDB00000NkvXXu0mjfMLs6V literal 0 HcmV?d00001 diff --git a/public/icon-maskable-512.png b/public/icon-maskable-512.png new file mode 100644 index 0000000000000000000000000000000000000000..9815f6e137816ce0bc03541c0bd08550ad612f69 GIT binary patch literal 54393 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�^pZ0KzDq-8r@CeC}_eRT-W3G&lsPq1v32c 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%0D3Th-oSRE(TocIq2)OxYR4Gt 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 \ No newline at end of file diff --git a/public/vercel.svg b/public/vercel.svg deleted file mode 100644 index 7705396..0000000 --- a/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/window.svg b/public/window.svg deleted file mode 100644 index b2b2a44..0000000 --- a/public/window.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/scripts/paypal-setup-plans.mjs b/scripts/paypal-setup-plans.mjs deleted file mode 100644 index fbc6f00..0000000 --- a/scripts/paypal-setup-plans.mjs +++ /dev/null @@ -1,102 +0,0 @@ -// One-time PayPal setup: creates the product + the recurring billing plans and -// prints the plan IDs to paste into your environment. -// -// 1. Set PAYPAL_CLIENT_ID / PAYPAL_SECRET (and PAYPAL_ENVIRONMENT) in .env.local -// 2. node scripts/paypal-setup-plans.mjs -// 3. Copy the printed PAYPAL_*_PLAN_ID lines into .env.local / production env -// -// Amounts mirror the app's pricing (Pro $29/mo, Landlord $59/mo); yearly is -// billed at 10× monthly (~2 months free). Adjust in the PayPal dashboard if you -// want different annual pricing. Safe to re-run (it creates fresh plans). -import { config } from "dotenv" - -config({ path: ".env.local", quiet: true }) - -const ENV = process.env.PAYPAL_ENVIRONMENT === "live" ? "live" : "sandbox" -const BASE = ENV === "live" ? "https://api-m.paypal.com" : "https://api-m.sandbox.paypal.com" -const id = process.env.PAYPAL_CLIENT_ID -const secret = process.env.PAYPAL_SECRET - -if (!id || !secret) { - console.error("[paypal-setup] Set PAYPAL_CLIENT_ID and PAYPAL_SECRET in .env.local first.") - process.exit(1) -} - -async function getToken() { - const r = await fetch(`${BASE}/v1/oauth2/token`, { - method: "POST", - headers: { - Authorization: `Basic ${Buffer.from(`${id}:${secret}`).toString("base64")}`, - "Content-Type": "application/x-www-form-urlencoded", - }, - body: "grant_type=client_credentials", - }) - if (!r.ok) throw new Error(`auth ${r.status}: ${await r.text()}`) - return (await r.json()).access_token -} - -const token = await getToken() -const post = (path, body) => - fetch(`${BASE}${path}`, { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - Prefer: "return=representation", - }, - body: JSON.stringify(body), - }) - -console.log(`[paypal-setup] Environment: ${ENV}`) - -const prodRes = await post("/v1/catalogs/products", { - name: "Property Management Network", - description: "Property Management Network subscription", - type: "SERVICE", - category: "SOFTWARE", -}) -if (!prodRes.ok) { - console.error("[paypal-setup] product creation failed:", await prodRes.text()) - process.exit(1) -} -const product = await prodRes.json() -console.log(`[paypal-setup] Product: ${product.id}`) - -const AMOUNTS = { pro: 29, landlord: 59 } -const envLines = [] - -for (const plan of ["pro", "landlord"]) { - for (const interval of ["month", "year"]) { - const amount = interval === "month" ? AMOUNTS[plan] : AMOUNTS[plan] * 10 - const res = await post("/v1/billing/plans", { - product_id: product.id, - name: `${plan[0].toUpperCase()}${plan.slice(1)} ${interval === "month" ? "Monthly" : "Yearly"}`, - status: "ACTIVE", - billing_cycles: [ - { - frequency: { interval_unit: interval === "month" ? "MONTH" : "YEAR", interval_count: 1 }, - tenure_type: "REGULAR", - sequence: 1, - total_cycles: 0, - pricing_scheme: { fixed_price: { value: amount.toFixed(2), currency_code: "USD" } }, - }, - ], - payment_preferences: { - auto_bill_outstanding: true, - setup_fee_failure_action: "CONTINUE", - payment_failure_threshold: 2, - }, - }) - if (!res.ok) { - console.error(`[paypal-setup] plan ${plan}/${interval} failed:`, await res.text()) - continue - } - const p = await res.json() - const key = `PAYPAL_${plan.toUpperCase()}_${interval === "month" ? "MONTHLY" : "YEARLY"}_PLAN_ID` - console.log(` ${plan}/${interval} $${amount} → ${p.id}`) - envLines.push(`${key}=${p.id}`) - } -} - -console.log("\n[paypal-setup] Add these to your environment:\n") -console.log(envLines.join("\n")) diff --git a/sentry.edge.config.ts b/sentry.edge.config.ts new file mode 100644 index 0000000..3e969c4 --- /dev/null +++ b/sentry.edge.config.ts @@ -0,0 +1,13 @@ +// Sentry initialization for the Edge runtime (middleware / proxy.ts). +// Loaded by instrumentation.ts when NEXT_RUNTIME === "edge". +import * as Sentry from "@sentry/nextjs" + +const dsn = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN + +Sentry.init({ + dsn, + enabled: !!dsn, + environment: process.env.SENTRY_ENVIRONMENT || process.env.NODE_ENV, + tracesSampleRate: process.env.NODE_ENV === "production" ? 0.2 : 1.0, + sendDefaultPii: false, +}) diff --git a/sentry.server.config.ts b/sentry.server.config.ts new file mode 100644 index 0000000..d4f02fe --- /dev/null +++ b/sentry.server.config.ts @@ -0,0 +1,16 @@ +// Sentry initialization for the Node.js server runtime. +// Loaded by instrumentation.ts when NEXT_RUNTIME === "nodejs". +import * as Sentry from "@sentry/nextjs" + +const dsn = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN + +Sentry.init({ + dsn, + // Inert until a DSN is configured — safe to ship before Sentry is set up. + enabled: !!dsn, + environment: process.env.SENTRY_ENVIRONMENT || process.env.NODE_ENV, + // Performance tracing — sample less in production to control volume/cost. + tracesSampleRate: process.env.NODE_ENV === "production" ? 0.2 : 1.0, + // Don't attach PII (IP, cookies, request bodies) by default. + sendDefaultPii: false, +})