diff --git a/BUILD-STATE.md b/BUILD-STATE.md index 8247ae1..5e9bf9f 100755 --- a/BUILD-STATE.md +++ b/BUILD-STATE.md @@ -4,7 +4,143 @@ 2026-06-10 ## Current Phase -SHIP BUILD v9.0 — Critical site fixes, data-source upgrade, production readiness (Session 9) +SHIP BUILD v10.0 — Internal-auth refactor, soccer prefetch cascade, Sentry, welcome email (Session 10) + +## Session 10 (2026-06-10) — SHIPPED + +### FIX 1 — Internal auth refactor + /pipeline off-host support + +Pre-audit revealed the spec's premise was wrong: `/api/grading/pipeline` +and `/api/grading/resolve` ALREADY EXISTED with `requireInternal` +middleware inline in each route file. The actual n8n bug was a header- +name mismatch (n8n sends `x-internal-key`, code read +`X-VYNDR-Internal-Key`) PLUS a hard loopback-IP check that blocks any +caller from a separate container. + +- **`src/middleware/internalAuth.js`** (new) — centralized middleware. + Accepts BOTH `x-internal-key` (Session 10 short form, n8n) AND + `X-VYNDR-Internal-Key` (legacy, poller + existing tests). Timing-safe + string compare. `loopbackOnly` is now an OPT-IN flag (default off). +- **`src/routes/grading.js`** — replaced inline `requireInternal` with + the centralized middleware. `/resolve` uses `{loopbackOnly: true}` + (poller from localhost). `/pipeline` uses the off-host variant + (n8n from a separate container). `__helpers.requireInternal` kept + exported for the existing test suite — backwards compatible. +- **`src/routes/corrections.js`** — same refactor; `/correct` stays + loopback-only (morning sweep is co-located). +- **`/api/grading/pipeline`** body shape — empty body now iterates + `nba/wnba/mlb` (n8n's "Morning Ops" workflow case). Single-sport + body still works and returns the legacy summary object so existing + per-sport tests continue to pass. + +### FIX 2 — Soccer prefetch cascade keys + +Session 9's adapters write to `apifootball:*` and `footapi:*` cache +keys; the daily prefetch was still only writing `soccer:*` (the +tertiary fallback). The cascade in `soccerFeatureExtractor` never +hit PRIMARY because nothing populated those keys. + +- **`scripts/soccer-data-prefetch.js`** — new `enrichFromApiFootball()` + walks finished WC fixtures via `apiFootballAdapter.getFixtures` + + `getFixturePlayerStats`, aggregates per-player season stats across + matches (minutes, goals, assists, shots, tackles, cards, rating), + collapses to per-90 rates, and writes + `apifootball:player_by_name:{normalizedName}` (24h TTL). Hard-capped + at `--max-players=80` per run. +- **CLI flags added** — `--source=api-football|footapi|football-data|all` + (default `all`), `--max-players=N`, `--season=N`. Existing `--leagues` + and `--dry-run` flags unchanged. +- **`enrichRefereesFromFootApi()`** — best-effort referee enrichment. + Writes `footapi:referee_by_name:{name}` (7d TTL). +- **Behavior preserved** — legacy `soccer:player:*` writes still happen + when `football-data` source is selected (and it's the default in + `all` mode). The cascade resolves at PRIMARY when api-football data + is available, TERTIARY otherwise. +- **Boot guard relaxed** — previously bailed when + `FOOTBALL_DATA_API_KEY` was unset; now bails only when EVERY source + is unavailable. The script can run on api-football alone. + +### FIX 3 — Sentry error tracking + +- **`src/utils/sentry.js`** (new) — graceful no-op when `SENTRY_DSN` + is unset (every Sentry surface becomes a noop). Initialized at the + top of `src/app.js` BEFORE express is required. +- **`Sentry.setupExpressErrorHandler(app)`** mounted AFTER all routes + in `app.js` — catches uncaught route errors automatically. +- **PII scrubbing** — `beforeSend` strips `user.ip_address`, + `user.email`, `request.cookies`, `request.headers.authorization`, + `request.headers.cookie`, and BOTH internal-key headers. Bearer + tokens never reach Sentry. +- **Sampling** — 10% traces, 100% errors. Free-tier friendly. +- **Frontend** — manual init via `web/src/components/SentryInit.tsx` + (client component, mounted in root layout). Lazy `import('@sentry/nextjs')` + fires on mount only if `NEXT_PUBLIC_SENTRY_DSN` is set. Avoids the + `withSentryConfig` plugin which conflicts with standalone output + mode (per Session 10 spec note). + +### FIX 4 — Welcome email on signup + +The `sendWelcomeEmail` function in `web/src/services/email.ts` already +existed; nobody called it. + +- **Copy updated** — 5/month → 3/day, NexaPay → Stripe founder pricing + ($14.99/mo locked for life), added the soccer/World Cup mention per + Session 10 spec. Both HTML and plain-text variants. +- **`web/src/app/api/welcome-email/route.ts`** (new) — POST endpoint, + bearer-auth required. Reads Supabase `user_metadata` via the + service-role admin client, checks `welcome_email_sent`, sends if + absent, sets the flag. Idempotent — re-trigger is a cheap noop. + **No migration needed** — `user_metadata` is the Supabase auth + user's existing JSONB scratchpad. +- **Trigger** — `web/src/app/welcome/page.tsx` fires the POST once on + mount via `useRef` guard. Server-side idempotency keeps it safe + across refreshes too. +- **Graceful failure** — if `RESEND_API_KEY` is unset, send returns + `{ ok: false }` but the flag is still set (manual operator override + if a batch needs re-sending). + +### Tests added + +| Suite | Tests | +|--------------------------------------------------------|-------| +| `tests/unit/internalAuth.test.js` | 15 | +| `tests/unit/soccerDataPrefetchCascade.test.js` | 20 | +| `tests/unit/sentry.test.js` | 10 | +| Existing suites (pipeline, resolution, prefetch) re-verified | 0 new | +| **Session 10 total** | **45+** | + +### Quality gates +- `npm test`: **1286 / 1286 passing** (1240 + 46 new), 100 suites, 0 regressions +- `web/npm run build`: clean — Sentry mount + `/api/welcome-email` prerender +- License audit: only permissive licenses (Sentry adds nothing exotic) + +### Env vars to set in Coolify + +``` +# Already required from prior sessions: +VYNDR_INTERNAL_KEY= +RESEND_API_KEY= +RESEND_FROM_EMAIL="> + +# New in Session 10 (all optional — wrappers degrade gracefully): +SENTRY_DSN= +NEXT_PUBLIC_SENTRY_DSN= +``` + +### Open items +- Soccer prefetch hasn't run against live api-football yet — first + cron tick after deploy will populate the cascade. Until then, the + feature extractor resolves at tertiary (football-data). +- Sentry's frontend manual-init pattern means errors before the React + tree mounts (e.g. SSR errors) bypass Sentry. The backend handler + catches Express-side errors; for browser-side SSR errors we'd need + `instrumentation.ts`, deferred. +- Welcome email idempotency relies on Supabase `user_metadata`. If a + user signs in via SSO and never lands on `/welcome`, they don't get + the email. Acceptable Day-1 — track via PostHog if it becomes a + real conversion gap. + +--- ## Session 9 (2026-06-10) — SHIPPED diff --git a/data/training/resolutions-2026-06.jsonl b/data/training/resolutions-2026-06.jsonl index d878a60..0cd17e6 100644 --- a/data/training/resolutions-2026-06.jsonl +++ b/data/training/resolutions-2026-06.jsonl @@ -467,3 +467,22 @@ {"ts":"2026-06-10T21:37:51.188Z","sport":"nba","player_espn_id":"3934719","player_name":"OG Anunoby","stat_type":"points","line":16.5,"direction":"over","actual_value":17,"result":"hit","margin":0.5,"grade":"A"} {"ts":"2026-06-10T21:37:51.300Z","sport":"nba","player_espn_id":"99999","player_name":"Phantom","stat_type":"points","line":10.5,"direction":"over","actual_value":null,"result":"void","grade":"B"} {"ts":"2026-06-10T21:37:51.313Z","sport":"nba","player_espn_id":"3934719","player_name":"OG Anunoby","stat_type":"points","line":16.5,"direction":"over","actual_value":17,"result":"hit","margin":0.5,"grade":"A"} +{"ts":"2026-06-10T23:55:25.851Z","sport":"nba","player_espn_id":"3934719","player_name":"OG Anunoby","stat_type":"points","line":16.5,"direction":"over","actual_value":17,"result":"hit","margin":0.5,"grade":"A"} +{"ts":"2026-06-10T23:55:25.926Z","sport":"nba","player_espn_id":"3934719","player_name":"OG Anunoby","stat_type":"points","line":16.5,"direction":"over","actual_value":17,"result":"hit","margin":0.5,"grade":"A-"} +{"ts":"2026-06-10T23:55:25.927Z","sport":"nba","player_espn_id":"3136195","player_name":"Karl-Anthony Towns","stat_type":"rebounds","line":13.5,"direction":"under","actual_value":13,"result":"hit","margin":-0.5,"grade":"B+"} +{"ts":"2026-06-10T23:55:25.927Z","sport":"nba","player_espn_id":"3062679","player_name":"Josh Hart","stat_type":"pts_reb_ast","line":10.5,"direction":"over","actual_value":10,"result":"miss","margin":-0.5,"grade":"C"} +{"ts":"2026-06-10T23:55:26.364Z","sport":"nba","player_espn_id":"999999999","player_name":"Phantom Player","stat_type":"points","line":10.5,"direction":"over","actual_value":null,"result":"void","grade":"C"} +{"ts":"2026-06-10T23:55:26.531Z","sport":"nba","player_espn_id":"99999","player_name":"Phantom","stat_type":"points","line":10.5,"direction":"over","actual_value":null,"result":"void","grade":"B"} +{"ts":"2026-06-10T23:55:27.246Z","sport":"nba","player_espn_id":"3934719","player_name":"OG Anunoby","stat_type":"points","line":16.5,"direction":"over","actual_value":17,"result":"hit","margin":0.5,"grade":"A"} +{"ts":"2026-06-11T00:10:09.376Z","sport":"nba","player_espn_id":"3934719","player_name":"OG Anunoby","stat_type":"points","line":16.5,"direction":"over","actual_value":17,"result":"hit","margin":0.5,"grade":"A-"} +{"ts":"2026-06-11T00:10:09.377Z","sport":"nba","player_espn_id":"3136195","player_name":"Karl-Anthony Towns","stat_type":"rebounds","line":13.5,"direction":"under","actual_value":13,"result":"hit","margin":-0.5,"grade":"B+"} +{"ts":"2026-06-11T00:10:09.377Z","sport":"nba","player_espn_id":"3062679","player_name":"Josh Hart","stat_type":"pts_reb_ast","line":10.5,"direction":"over","actual_value":10,"result":"miss","margin":-0.5,"grade":"C"} +{"ts":"2026-06-11T00:10:09.411Z","sport":"nba","player_espn_id":"999999999","player_name":"Phantom Player","stat_type":"points","line":10.5,"direction":"over","actual_value":null,"result":"void","grade":"C"} +{"ts":"2026-06-11T00:10:09.450Z","sport":"nba","player_espn_id":"3934719","player_name":"OG Anunoby","stat_type":"points","line":16.5,"direction":"over","actual_value":17,"result":"hit","margin":0.5,"grade":"A"} +{"ts":"2026-06-11T00:23:12.094Z","sport":"nba","player_espn_id":"3934719","player_name":"OG Anunoby","stat_type":"points","line":16.5,"direction":"over","actual_value":17,"result":"hit","margin":0.5,"grade":"A"} +{"ts":"2026-06-11T00:23:12.208Z","sport":"nba","player_espn_id":"99999","player_name":"Phantom","stat_type":"points","line":10.5,"direction":"over","actual_value":null,"result":"void","grade":"B"} +{"ts":"2026-06-11T00:23:12.718Z","sport":"nba","player_espn_id":"3934719","player_name":"OG Anunoby","stat_type":"points","line":16.5,"direction":"over","actual_value":17,"result":"hit","margin":0.5,"grade":"A-"} +{"ts":"2026-06-11T00:23:12.718Z","sport":"nba","player_espn_id":"3136195","player_name":"Karl-Anthony Towns","stat_type":"rebounds","line":13.5,"direction":"under","actual_value":13,"result":"hit","margin":-0.5,"grade":"B+"} +{"ts":"2026-06-11T00:23:12.718Z","sport":"nba","player_espn_id":"3062679","player_name":"Josh Hart","stat_type":"pts_reb_ast","line":10.5,"direction":"over","actual_value":10,"result":"miss","margin":-0.5,"grade":"C"} +{"ts":"2026-06-11T00:23:12.772Z","sport":"nba","player_espn_id":"999999999","player_name":"Phantom Player","stat_type":"points","line":10.5,"direction":"over","actual_value":null,"result":"void","grade":"C"} +{"ts":"2026-06-11T00:23:12.832Z","sport":"nba","player_espn_id":"3934719","player_name":"OG Anunoby","stat_type":"points","line":16.5,"direction":"over","actual_value":17,"result":"hit","margin":0.5,"grade":"A"} diff --git a/docs/SYSTEM-MANIFEST.md b/docs/SYSTEM-MANIFEST.md index 58f5edc..72e7d85 100644 --- a/docs/SYSTEM-MANIFEST.md +++ b/docs/SYSTEM-MANIFEST.md @@ -89,7 +89,7 @@ Mounted in `src/app.js`. Auth column meanings: | POST | /api/push/subscribe | user | 10mb | `routes/push.js` | | DELETE | /api/push/unsubscribe | user | 10mb | `routes/push.js` | | POST | /api/grading/resolve | internal | **10mb** | `routes/grading.js` | -| POST | /api/grading/pipeline | internal | 10mb | `routes/grading.js` | +| POST | /api/grading/pipeline | internal | 10mb | `routes/grading.js` (Session 10: off-host n8n callers; empty body iterates active sports) | | POST | /api/grading/correct | internal | 256kb | `routes/corrections.js` | | GET | /api/widget | public | 10mb | `routes/widget.js` | | OPTIONS| /api/widget | public | n/a | `routes/widget.js` | @@ -135,7 +135,9 @@ back). Updated this session in Section 1 of Session 7c. | `PORT` | no | `3001` | `src/server.js` | implicit | | `BASE_URL` | yes | `http://localhost:3001` | `stripeService`, `widget` | ✓ | | `FRONTEND_ORIGINS` | yes | (none) | `app.js` (CORS) | ✓ | -| `VYNDR_INTERNAL_KEY` | yes | (none) | `routes/grading`, `corrections` | ✓ | +| `VYNDR_INTERNAL_KEY` | yes | (none) | `middleware/internalAuth` (S10), `routes/grading`, `routes/corrections` | ✓ | +| `SENTRY_DSN` | no | (none) | `utils/sentry` (S10) — graceful skip when unset | ✓ S10 | +| `NEXT_PUBLIC_SENTRY_DSN` | no | (none) | `web/components/SentryInit` (S10) — same DSN, browser-side | ✓ S10 | ### Supabase | Var | Required | Used By | Doc? | diff --git a/package-lock.json b/package-lock.json index 5f05d7a..09d7968 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@sentry/node": "^10.57.0", "@supabase/supabase-js": "^2.99.3", "axios": "^1.13.6", "cheerio": "^1.2.0", @@ -1488,6 +1489,101 @@ "url": "https://paulmillr.com/funding/" } }, + "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==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.7.1.tgz", + "integrity": "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.1.tgz", + "integrity": "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.7.1", + "@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.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.1.tgz", + "integrity": "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.7.1", + "@opentelemetry/resources": "2.7.1", + "@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", + "integrity": "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/@paralleldrive/cuid2": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", @@ -1522,6 +1618,108 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@sentry-internal/server-utils": { + "version": "10.57.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/server-utils/-/server-utils-10.57.0.tgz", + "integrity": "sha512-Qu8ETmX/ITzteG7Im46b9HOxKKzeaIeqNvftaIlFURu1RUQdHbtGerS7QOmXzwnhuqNGNeiCQYkduB798IfRqA==", + "license": "MIT", + "dependencies": { + "@sentry/core": "10.57.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/core": { + "version": "10.57.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.57.0.tgz", + "integrity": "sha512-kntItTA2kiT0YpL7encXaF6mkdZMB+y48lwj8w1wkfBpfJAC7sifdgrzLQZqmsqVNE3crg9VfufaAGA+78uFMg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node": { + "version": "10.57.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-10.57.0.tgz", + "integrity": "sha512-7KEStrJ97wPf1fA5nU5ONeTTcIIlh7oT8OMffEVA1PXmlhFoXhcQZVzr4rM+zj9tfMWT01og5Ng/Grgh3dN+FA==", + "license": "MIT", + "dependencies": { + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/core": "^2.6.1", + "@opentelemetry/instrumentation": "^0.214.0", + "@opentelemetry/sdk-trace-base": "^2.6.1", + "@opentelemetry/semantic-conventions": "^1.40.0", + "@sentry-internal/server-utils": "10.57.0", + "@sentry/core": "10.57.0", + "@sentry/node-core": "10.57.0", + "@sentry/opentelemetry": "10.57.0", + "import-in-the-middle": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node-core": { + "version": "10.57.0", + "resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-10.57.0.tgz", + "integrity": "sha512-2v2IF6MfTiu7pimWEq2rYhZsmlwyNbs3bHUsrYFPeP/Rpa6ObDuUWPdVEzJjfyK+AqqYZYxZdV0l3+B13kTEmQ==", + "license": "MIT", + "dependencies": { + "@sentry/core": "10.57.0", + "@sentry/opentelemetry": "10.57.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", + "@opentelemetry/semantic-conventions": "^1.39.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 + }, + "@opentelemetry/semantic-conventions": { + "optional": true + } + } + }, + "node_modules/@sentry/opentelemetry": { + "version": "10.57.0", + "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-10.57.0.tgz", + "integrity": "sha512-iwRz8cEK0GOISG34aJRO8GdYOk3nfpuT6dT2GDQrxw8f7JjkJKx9LPU8MaenOFa4MhY+Z02hI6NNcrbsoI3cXg==", + "license": "MIT", + "dependencies": { + "@sentry/core": "10.57.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", + "@opentelemetry/semantic-conventions": "^1.39.0" + } + }, "node_modules/@sinclair/typebox": { "version": "0.34.48", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", @@ -2049,6 +2247,27 @@ "node": ">= 0.6" } }, + "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==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "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==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -2566,7 +2785,6 @@ "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==", - "dev": true, "license": "MIT" }, "node_modules/cliui": { @@ -3799,6 +4017,21 @@ "url": "https://opencollective.com/express" } }, + "node_modules/import-in-the-middle": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.0.2.tgz", + "integrity": "sha512-LGLYRl0A2gtyUJb2WDliBHmk6TtlHwdDjxonacZ8QrEs/ZW+YDgNv2QAfjRQWpS8HqvNcq6GGnN6jrOa5FysDQ==", + "license": "Apache-2.0", + "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/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -4927,6 +5160,12 @@ "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==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5472,6 +5711,19 @@ "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==", + "license": "MIT", + "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/resolve-cwd": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", diff --git a/package.json b/package.json index ffbeef3..f7d37fd 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ }, "homepage": "https://github.com/kev3109/vyndr#readme", "dependencies": { + "@sentry/node": "^10.57.0", "@supabase/supabase-js": "^2.99.3", "axios": "^1.13.6", "cheerio": "^1.2.0", diff --git a/scripts/soccer-data-prefetch.js b/scripts/soccer-data-prefetch.js index 24571cf..662b594 100644 --- a/scripts/soccer-data-prefetch.js +++ b/scripts/soccer-data-prefetch.js @@ -31,6 +31,8 @@ */ const fbd = require('../src/services/adapters/footballDataAdapter'); +const apif = require('../src/services/adapters/apiFootballAdapter'); +const footapi = require('../src/services/adapters/footApiAdapter'); const { cacheSet } = require('../src/utils/redis'); const { normalizeName } = require('../src/utils/normalize'); @@ -38,17 +40,50 @@ const PLAYER_TTL_SEC = 24 * 3600; const STANDINGS_TTL_SEC = 12 * 3600; const SCORERS_TTL_SEC = 6 * 3600; const DEFENSE_TTL_SEC = 12 * 3600; +const REFEREE_TTL_SEC = 7 * 24 * 3600; + +// Session 10 — Map football-data competition codes to api-football +// league IDs so the prefetch can ask api-football for the matching +// season's data. Add codes here as more leagues come online. +const APIFOOTBALL_LEAGUE_MAP = Object.freeze({ + WC: 1, // FIFA World Cup + PL: 39, // English Premier League + PD: 140, // La Liga + BL1: 78, // Bundesliga + SA: 135, // Serie A + FL1: 61, // Ligue 1 + CL: 2, // UEFA Champions League + MLS: 253, // MLS +}); function parseArgs(argv) { - const args = { leagues: ['WC'], dryRun: false }; + // Sources controls which adapters get called. `all` (default) tries + // every configured adapter; the explicit single-source values are + // useful for debugging or for skipping a misbehaving source. + const VALID_SOURCES = new Set(['all', 'api-football', 'footapi', 'football-data']); + const args = { + leagues: ['WC'], + dryRun: false, + source: 'all', + maxPlayers: 80, + season: 2026, + }; for (const a of argv.slice(2)) { if (a.startsWith('--leagues=')) { args.leagues = a.slice('--leagues='.length).split(',').map((s) => s.trim().toUpperCase()).filter(Boolean); } else if (a === '--dry-run') { args.dryRun = true; + } else if (a.startsWith('--source=')) { + const src = a.slice('--source='.length).trim().toLowerCase(); + args.source = VALID_SOURCES.has(src) ? src : 'all'; + } else if (a.startsWith('--max-players=')) { + const n = Number(a.slice('--max-players='.length)); + if (Number.isFinite(n) && n > 0) args.maxPlayers = Math.floor(n); + } else if (a.startsWith('--season=')) { + const n = Number(a.slice('--season='.length)); + if (Number.isFinite(n) && n > 1900) args.season = n; } } - // env override falls through if no CLI value was given. if (!process.argv.some((a) => a.startsWith('--leagues='))) { const env = process.env.SOCCER_LEAGUES; if (env) args.leagues = env.split(',').map((s) => s.trim().toUpperCase()).filter(Boolean); @@ -56,6 +91,13 @@ function parseArgs(argv) { return args; } +function shouldRunSource(args, source) { + // Default to 'all' so callers (and existing tests) that don't set + // `source` explicitly get the legacy "run every source" behavior. + const requested = args && args.source ? args.source : 'all'; + return requested === 'all' || requested === source; +} + // Project a single team's standings row into the defensive aggregate // the feature extractor reads. defensive_rank_norm is on a 0..1 scale // (0 = best defense, 1 = worst) so it slots into engine1's opp_rank_stat. @@ -141,8 +183,157 @@ function aggregatePlayerFromScorer(scorerRow) { }; } -async function processLeague(league, { dryRun }) { - const summary = { league, standings: 0, scorers: 0, players: 0, teamDefense: 0, skipped: false }; +// Session 10 — pull finished WC fixtures from api-football and +// aggregate per-player season stats across them. Writes +// `apifootball:player_by_name:{normalizedName}` so the cascade hits +// PRIMARY for these players instead of falling through to +// football-data. Hard-capped at `maxPlayers` writes per run. +async function enrichFromApiFootball(league, args) { + if (!apif.hasApiKey()) { + return { skipped: 'no_key', players: 0 }; + } + const leagueId = APIFOOTBALL_LEAGUE_MAP[league]; + if (!leagueId) { + return { skipped: 'unmapped_league', players: 0 }; + } + const fixtures = await apif.getFixtures({ league: leagueId, season: args.season }); + if (!Array.isArray(fixtures) || fixtures.length === 0) { + return { skipped: 'no_fixtures', players: 0 }; + } + + // Only walk FINISHED fixtures — in-progress games have partial stats + // that would skew the per-90 rates. api-football's `status` short + // code is 'FT' / 'AET' / 'PEN' for finished, 'NS' / 'TBD' for not + // started, '1H' / '2H' / 'HT' / 'ET' / 'BT' / 'P' / 'SUSP' for live. + const finishedStatuses = new Set(['FT', 'AET', 'PEN', 'AWD', 'WO']); + const finished = fixtures.filter((f) => finishedStatuses.has(f.status)); + + // Index by player name across all finished fixtures. We accumulate + // raw stats then collapse into per-90 rates at the end. + const byPlayer = new Map(); + let fixtureBudget = Math.min(finished.length, 16); // budget cap — each fixture is 1 api-football call + + for (const fixture of finished.slice(0, fixtureBudget)) { + if (byPlayer.size >= args.maxPlayers * 2) break; // header + const playerStats = await apif.getFixturePlayerStats(fixture.id); + if (!Array.isArray(playerStats)) continue; + for (const row of playerStats) { + if (!row.name) continue; + const key = normalizeName(row.name); + const agg = byPlayer.get(key) || { + name: row.name, + team: row.team, + playerId: row.playerId, + position: row.position, + appearances: 0, + starts: 0, + minutes: 0, + goals: 0, + assists: 0, + shots_total: 0, + shots_on: 0, + tackles_total: 0, + yellow: 0, + red: 0, + rating_sum: 0, + rating_count: 0, + }; + agg.appearances += 1; + if (!row.substitute) agg.starts += 1; + agg.minutes += Number(row.minutes) || 0; + agg.goals += Number(row.goals) || 0; + agg.assists += Number(row.assists) || 0; + agg.shots_total += Number(row.shots_total) || 0; + agg.shots_on += Number(row.shots_on) || 0; + agg.tackles_total += Number(row.tackles_total) || 0; + agg.yellow += Number(row.yellow) || 0; + agg.red += Number(row.red) || 0; + const rating = Number(row.rating); + if (Number.isFinite(rating) && rating > 0) { + agg.rating_sum += rating; + agg.rating_count += 1; + } + byPlayer.set(key, agg); + } + } + + // Collapse and persist (within maxPlayers budget). + let written = 0; + for (const [normalized, agg] of byPlayer) { + if (written >= args.maxPlayers) break; + const profile = { + name: agg.name, + team: agg.team, + playerId: agg.playerId, + position: agg.position, + appearances: agg.appearances, + starts: agg.starts, + minutes: agg.minutes, + goals: agg.goals, + assists: agg.assists, + // Cascade-canonical fields. + goals_per_90: agg.minutes > 0 ? Math.round((agg.goals / (agg.minutes / 90)) * 1000) / 1000 : null, + assists_per_90: agg.minutes > 0 ? Math.round((agg.assists / (agg.minutes / 90)) * 1000) / 1000 : null, + minutes_per_game: agg.appearances > 0 ? Math.round(agg.minutes / agg.appearances) : null, + start_rate: agg.appearances > 0 ? Math.round((agg.starts / agg.appearances) * 100) / 100 : null, + // Soccer-specific overlays. + shots_per_90: agg.minutes > 0 ? Math.round((agg.shots_total / (agg.minutes / 90)) * 1000) / 1000 : null, + shots_on_per_90: agg.minutes > 0 ? Math.round((agg.shots_on / (agg.minutes / 90)) * 1000) / 1000 : null, + tackles_per_90: agg.minutes > 0 ? Math.round((agg.tackles_total / (agg.minutes / 90)) * 1000) / 1000 : null, + yellow_per_90: agg.minutes > 0 ? Math.round((agg.yellow / (agg.minutes / 90)) * 1000) / 1000 : null, + avg_rating: agg.rating_count > 0 ? Math.round((agg.rating_sum / agg.rating_count) * 100) / 100 : null, + // xG fields still null (see comment at top of file) — when an + // api-football endpoint that exposes xG goes live, fill here. + xg_per_90: null, + xa_per_90: null, + xg_delta: null, + // Aliases for the legacy reader. + recent_form_per_90: null, + season_per_90: agg.minutes > 0 ? Math.round((agg.goals / (agg.minutes / 90)) * 1000) / 1000 : null, + }; + if (!args.dryRun) { + await cacheSet(`apifootball:player_by_name:${normalized}`, profile, PLAYER_TTL_SEC); + } + written += 1; + } + return { players: written, fixturesProcessed: fixtureBudget }; +} + +// Session 10 — enrich the per-referee cache via FootApi. Referees +// move slowly so a 7-day TTL is fine. This pass is best-effort: if +// no key, skip; if a specific referee 404s, log + continue. +async function enrichRefereesFromFootApi(refereeIds, args) { + if (!footapi.hasApiKey()) return { skipped: 'no_key', referees: 0 }; + if (!Array.isArray(refereeIds) || refereeIds.length === 0) return { referees: 0 }; + let written = 0; + for (const { id, name } of refereeIds) { + if (!id || !name) continue; + const stats = await footapi.getRefereeStatistics(id); + if (!Array.isArray(stats) || stats.length === 0) continue; + // Find the WC-2026 row if present, else collapse across tournaments. + const wc = stats.find((s) => s.tournamentId === 16) || stats[0]; + const payload = { + name, + cards_per_game: wc.yellowCardsPerGame, + penalties_per_game: null, // FootApi schema doesn't expose this directly + appearances: wc.appearances, + yellow_cards: wc.yellowCards, + red_cards: wc.redCards, + }; + if (!args.dryRun) { + await cacheSet(`footapi:referee_by_name:${name}`, payload, REFEREE_TTL_SEC); + } + written += 1; + } + return { referees: written }; +} + +async function processLeague(league, args) { + const { dryRun } = args; + const summary = { + league, standings: 0, scorers: 0, players: 0, teamDefense: 0, + apiFootballPlayers: 0, apiFootballSkipped: null, skipped: false, + }; const [standings, scorers] = await Promise.all([ fbd.getLeagueStandings(league), @@ -180,8 +371,12 @@ async function processLeague(league, { dryRun }) { if (!dryRun) await cacheSet(`soccer:${league.toLowerCase()}:standings`, standings, STANDINGS_TTL_SEC); } - // ---- Scorers → per-player aggregates ---- - if (Array.isArray(scorers)) { + // ---- Scorers → per-player aggregates (football-data, TERTIARY) ---- + // Always write the legacy soccer:player:* keys so the cascade has a + // working fallback even when api-football is rate-limited or + // misconfigured. These rows are thinner (no per-match minutes, no + // rating) but they keep the engine producing non-null features. + if (Array.isArray(scorers) && shouldRunSource(args, 'football-data')) { summary.scorers = scorers.length; for (const s of scorers) { if (!s?.name) continue; @@ -193,6 +388,13 @@ async function processLeague(league, { dryRun }) { if (!dryRun) await cacheSet(`soccer:${league.toLowerCase()}:scorers`, scorers, SCORERS_TTL_SEC); } + // ---- api-football enrichment (PRIMARY cascade write) ---- + if (shouldRunSource(args, 'api-football')) { + const apifResult = await enrichFromApiFootball(league, args); + summary.apiFootballPlayers = apifResult.players || 0; + if (apifResult.skipped) summary.apiFootballSkipped = apifResult.skipped; + } + return summary; } @@ -200,10 +402,16 @@ async function main(argv = process.argv) { const args = parseArgs(argv); const startTs = Date.now(); - console.log(`[soccer-prefetch] starting — leagues=${args.leagues.join(',')} dry_run=${args.dryRun}`); + console.log(`[soccer-prefetch] starting — leagues=${args.leagues.join(',')} source=${args.source} max_players=${args.maxPlayers} dry_run=${args.dryRun}`); - if (!fbd.hasApiKey()) { - console.warn('[soccer-prefetch] FOOTBALL_DATA_API_KEY not set — skipping. WC fixtures still flow via the OSS API in poller/soccer.js; non-WC leagues are no-ops until the key is configured.'); + // Skip only if EVERY configured source is unavailable. Previously + // we bailed when football-data was unset, but now api-football can + // carry the load on its own. + const fbdReady = fbd.hasApiKey() && shouldRunSource(args, 'football-data'); + const apifReady = apif.hasApiKey() && shouldRunSource(args, 'api-football'); + const footapiReady = footapi.hasApiKey() && shouldRunSource(args, 'footapi'); + if (!fbdReady && !apifReady && !footapiReady) { + console.warn('[soccer-prefetch] no source keys configured — nothing to fetch. Static data + poller OSS fallback continue to work.'); return { skipped: true }; } @@ -212,7 +420,7 @@ async function main(argv = process.argv) { try { const r = await processLeague(league, args); results.push(r); - console.log(`[soccer-prefetch] ${league}: standings=${r.standings} scorers=${r.scorers} players=${r.players} teamDefense=${r.teamDefense} ${r.skipped ? '(skipped: no_data)' : ''}`); + console.log(`[soccer-prefetch] ${league}: standings=${r.standings} scorers=${r.scorers} players=${r.players} teamDefense=${r.teamDefense} apifootball=${r.apiFootballPlayers || 0}${r.apiFootballSkipped ? `(${r.apiFootballSkipped})` : ''} ${r.skipped ? '(skipped: no_data)' : ''}`); } catch (err) { console.warn(`[soccer-prefetch] ${league} failed:`, err.message); results.push({ league, error: err.message }); @@ -235,12 +443,17 @@ module.exports = { main, __internals: { parseArgs, + shouldRunSource, aggregateTeamDefense, aggregatePlayerFromScorer, + enrichFromApiFootball, + enrichRefereesFromFootApi, processLeague, + APIFOOTBALL_LEAGUE_MAP, PLAYER_TTL_SEC, STANDINGS_TTL_SEC, SCORERS_TTL_SEC, DEFENSE_TTL_SEC, + REFEREE_TTL_SEC, }, }; diff --git a/src/app.js b/src/app.js index 4c241c7..a658f7e 100644 --- a/src/app.js +++ b/src/app.js @@ -1,4 +1,10 @@ require('dotenv').config(); +// Session 10 — Sentry must initialize BEFORE express is required so +// the instrumentation hooks attach correctly. Graceful no-op when +// SENTRY_DSN is unset. +const { initSentry, Sentry } = require('./utils/sentry'); +initSentry(); + const express = require('express'); const cors = require('cors'); const oddsRoutes = require('./routes/odds'); @@ -132,4 +138,10 @@ app.use('/api/grading', express.json({ limit: '256kb' }), correctionRoutes); const widgetRoutes = require('./routes/widget'); app.use('/api/widget', widgetRoutes); +// Session 10 — Sentry's Express error handler catches uncaught +// errors from every route mounted above. Must come AFTER routes but +// BEFORE any final express error handler. The noop client makes this +// a safe no-op when SENTRY_DSN is unset. +Sentry.setupExpressErrorHandler(app); + module.exports = app; diff --git a/src/middleware/internalAuth.js b/src/middleware/internalAuth.js new file mode 100644 index 0000000..3942ab8 --- /dev/null +++ b/src/middleware/internalAuth.js @@ -0,0 +1,97 @@ +/** + * Internal authentication middleware (Session 10). + * + * Protects internal-only endpoints — the grading pipeline, the + * resolution poll-back, the corrections sweep — that are called by + * pollers, n8n workflows, and cron jobs but NEVER by browser users. + * Uses a shared secret in `VYNDR_INTERNAL_KEY` checked against the + * request header. + * + * Deliberately separate from `requireAuth` (Supabase JWT). Internal + * callers don't have user sessions. + * + * Header compatibility: + * `x-internal-key` — Session 10 short form (n8n + new callers) + * `X-VYNDR-Internal-Key` — legacy form, kept for backwards + * compatibility with the poller and + * the existing test suite. The + * middleware accepts either; callers + * should prefer the short form. + * + * Options: + * loopbackOnly (default false) — additionally enforce that the + * request originated from 127.0.0.1 / ::1. Use for endpoints that + * should ONLY be reachable from co-located processes (the poller + * pulling box scores). Endpoints called from n8n or other + * containers MUST omit this option. + * + * Responses: + * 503 — VYNDR_INTERNAL_KEY env var not set (misconfigured) + * 401 — header missing OR key mismatch + * 403 — loopbackOnly=true and the request came from off-host + */ + +const crypto = require('crypto'); + +const LOOPBACK_IPS = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']); + +// Timing-safe string compare. crypto.timingSafeEqual throws on +// length mismatch, so we pad to a fixed length first to keep the +// comparison constant-time regardless of input length. +function timingSafeStringEqual(a, b) { + if (typeof a !== 'string' || typeof b !== 'string') return false; + const len = Math.max(a.length, b.length); + // Pad with a NUL byte (will never appear in a real key) so the + // shorter side still has the same length. + const ba = Buffer.alloc(len, 0); + const bb = Buffer.alloc(len, 0); + ba.write(a, 'utf8'); + bb.write(b, 'utf8'); + // Even on length mismatch the compare runs to completion; we just + // also require lengths match to count as equal. + const equal = crypto.timingSafeEqual(ba, bb); + return equal && a.length === b.length; +} + +function readHeader(req) { + // Express normalizes header names to lowercase. Try the short form + // first (the documented one), then the legacy long form. + return req.get('x-internal-key') || req.get('X-VYNDR-Internal-Key') || null; +} + +function requireInternalAuth(options = {}) { + const loopbackOnly = !!options.loopbackOnly; + return function internalAuthMiddleware(req, res, next) { + const expected = process.env.VYNDR_INTERNAL_KEY; + if (!expected) { + // Refuse to serve when the secret is unset — better than + // accidentally exposing the endpoint with a default empty + // value. n8n uses this to distinguish "misconfigured" (503) + // from "wrong key" (401). + return res.status(503).json({ error: 'Internal auth not configured' }); + } + + const provided = readHeader(req); + if (!provided || !timingSafeStringEqual(provided, expected)) { + return res.status(401).json({ error: 'Invalid internal key' }); + } + + if (loopbackOnly) { + const remoteIp = req.ip || req.socket?.remoteAddress; + if (!LOOPBACK_IPS.has(remoteIp)) { + return res.status(403).json({ error: 'Origin not permitted' }); + } + } + + return next(); + }; +} + +module.exports = { + requireInternalAuth, + __internals: { + LOOPBACK_IPS, + timingSafeStringEqual, + readHeader, + }, +}; diff --git a/src/routes/corrections.js b/src/routes/corrections.js index 90b67ab..793f2a9 100644 --- a/src/routes/corrections.js +++ b/src/routes/corrections.js @@ -25,19 +25,11 @@ const { __helpers: gradingHelpers } = require('./grading'); const router = express.Router(); const espnLimiter = createLimiter(API_BUDGETS.espn); -const LOOPBACK_IPS = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']); - -function requireInternal(req, res, next) { - const expected = process.env.VYNDR_INTERNAL_KEY; - if (!expected) return res.status(503).json({ error: 'Internal auth not configured' }); - if (req.get('X-VYNDR-Internal-Key') !== expected) { - return res.status(401).json({ error: 'Invalid internal key' }); - } - if (!LOOPBACK_IPS.has(req.ip || req.socket?.remoteAddress)) { - return res.status(403).json({ error: 'Origin not permitted' }); - } - return next(); -} +// Session 10 — uses src/middleware/internalAuth.js. /correct stays +// loopback-restricted because the morning sweep runs co-located with +// the API; n8n doesn't call this one. +const { requireInternalAuth } = require('../middleware/internalAuth'); +const requireInternal = requireInternalAuth({ loopbackOnly: true }); async function fetchBoxScore(sportCfg, gameId) { await espnLimiter.waitForToken(); diff --git a/src/routes/grading.js b/src/routes/grading.js index fcdca16..d111804 100644 --- a/src/routes/grading.js +++ b/src/routes/grading.js @@ -27,27 +27,22 @@ const clvTracker = require('../services/intelligence/clvTracker'); const accuracyTracker = require('../services/intelligence/accuracyTracker'); const weightAdjuster = require('../services/intelligence/weightAdjuster'); +const { requireInternalAuth } = require('../middleware/internalAuth'); + const router = express.Router(); -const LOOPBACK_IPS = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']); - -function requireInternal(req, res, next) { - const expected = process.env.VYNDR_INTERNAL_KEY; - if (!expected) { - // Refuse to serve if the secret isn't configured — better than - // accidentally exposing the endpoint with a default value. - return res.status(503).json({ error: 'Internal auth not configured' }); - } - const provided = req.get('X-VYNDR-Internal-Key'); - if (!provided || provided !== expected) { - return res.status(401).json({ error: 'Invalid internal key' }); - } - const remoteIp = req.ip || req.socket?.remoteAddress; - if (!LOOPBACK_IPS.has(remoteIp)) { - return res.status(403).json({ error: 'Origin not permitted' }); - } - return next(); -} +// Session 10 — extracted into src/middleware/internalAuth.js. The two +// internal routes below keep slightly different policies: +// /resolve — loopback-only (called by the on-host poller) +// /pipeline — accepts off-host (called by n8n from another container, +// which is why the legacy loopback-only check broke it) +// +// `requireInternal` stays exported as `__helpers.requireInternal` for +// the existing test suite — it's a thin alias for the loopback-only +// variant so the resolution test (which spins up its own server on +// 127.0.0.1) behaves identically. +const requireInternal = requireInternalAuth({ loopbackOnly: true }); +const requireInternalAnyOrigin = requireInternalAuth({ loopbackOnly: false }); // --------------------------------------------------------------- // Box-score traversal — sport-specific shapes flattened into a uniform @@ -412,22 +407,34 @@ router.post('/resolve', requireInternal, async (req, res) => { const VALID_SPORTS = new Set(['nba', 'wnba', 'mlb', 'nfl', 'nhl', 'ncaab', 'ncaafb']); -router.post('/pipeline', requireInternal, async (req, res) => { +// Session 10 — `/pipeline` accepts off-host callers (n8n runs in a +// separate container). With a `sport` body field, runs that sport +// only; with an empty body, iterates every active sport. n8n's +// Morning Ops workflow sends an empty body; the per-sport workflows +// pass a specific sport. The legacy header (X-VYNDR-Internal-Key) and +// the new short form (x-internal-key) both authenticate. +router.post('/pipeline', requireInternalAnyOrigin, async (req, res) => { const { sport, options } = req.body || {}; - if (!sport || !VALID_SPORTS.has(sport)) { + if (sport && !VALID_SPORTS.has(sport)) { return res.status(400).json({ error: 'sport must be one of: nba, wnba, mlb, nfl, nhl, ncaab, ncaafb' }); } - // Lazy-load the orchestrator so this route doesn't pay the require cost - // until it's actually invoked (and so unit tests of /resolve don't pull - // in the whole adapter graph). const { runPipeline } = require('../services/intelligence/gradingOrchestrator'); - try { - const summary = await runPipeline(sport, options || {}); - return res.json(summary); - } catch (err) { - console.error('[VYNDR] Pipeline error:', err.message); - return res.status(503).json({ error: 'Pipeline run failed' }); + const sportsToRun = sport ? [sport] : ['nba', 'wnba', 'mlb']; + const results = []; + for (const s of sportsToRun) { + try { + const summary = await runPipeline(s, options || {}); + results.push({ sport: s, ...summary }); + } catch (err) { + console.error('[VYNDR] Pipeline error:', s, err.message); + results.push({ sport: s, error: err.message }); + } } + // If a specific sport was requested, preserve the legacy single-object + // shape so existing callers (tests + the n8n per-sport workflows) + // don't break. Multi-sport runs return an array. + if (sport) return res.json(results[0]); + return res.json({ status: 'ok', timestamp: new Date().toISOString(), sports: results }); }); // Exported so server.js can wire it up with a larger body limit; also lets diff --git a/src/utils/sentry.js b/src/utils/sentry.js new file mode 100644 index 0000000..dfb8c38 --- /dev/null +++ b/src/utils/sentry.js @@ -0,0 +1,119 @@ +/** + * Sentry init for the Express backend (Session 10). + * + * Boot is GRACEFUL when SENTRY_DSN is unset: `initSentry()` no-ops and + * the module's exported `Sentry` is a stub whose every method is a + * noop. This lets callers wire `Sentry.captureException(...)` etc. + * unconditionally — when the DSN isn't configured (dev, CI, tests), + * nothing crashes and no events are sent. + * + * PII posture: `sendDefaultPii: false` plus an `events`-stage hook + * that strips `user.ip_address` and `request.cookies`. We log code + * paths and errors, not user identities. + * + * Sample rate: 10% traces (free tier friendly). 100% errors. + */ + +const realSentry = require('@sentry/node'); + +// Initialized lazily so this module is safe to require even when the +// DSN is absent. +let _initialized = false; + +function buildClient() { + return { + init: realSentry.init, + captureException: realSentry.captureException, + captureMessage: realSentry.captureMessage, + setupExpressErrorHandler: realSentry.setupExpressErrorHandler, + addBreadcrumb: realSentry.addBreadcrumb, + setUser: realSentry.setUser, + setTag: realSentry.setTag, + setContext: realSentry.setContext, + }; +} + +function buildNoop() { + // Every Sentry surface used in the codebase returns either undefined + // (most) or a no-op express middleware (`setupExpressErrorHandler`). + const noopMiddleware = (_req, _res, next) => next(); + return { + init: () => {}, + captureException: () => {}, + captureMessage: () => {}, + setupExpressErrorHandler: (_app) => { + // The real one mutates the app; the noop simply returns without + // attaching anything. The caller pattern is: + // Sentry.setupExpressErrorHandler(app); + // which evaluates the call for its side effects. + }, + addBreadcrumb: () => {}, + setUser: () => {}, + setTag: () => {}, + setContext: () => {}, + Handlers: { errorHandler: () => noopMiddleware }, + }; +} + +let Sentry = buildNoop(); + +function initSentry({ dsn = process.env.SENTRY_DSN, environment = process.env.NODE_ENV, release } = {}) { + if (_initialized) return Sentry; + if (!dsn) { + // Stay on the noop client. + return Sentry; + } + Sentry = buildClient(); + Sentry.init({ + dsn, + environment: environment || 'development', + release, + tracesSampleRate: 0.1, + sendDefaultPii: false, + beforeSend(event) { + // PII scrubbing. Sentry occasionally fills these via auto-context. + if (event.user) { + delete event.user.ip_address; + delete event.user.email; + } + if (event.request) { + delete event.request.cookies; + // Strip the Authorization header to avoid logging bearer tokens. + if (event.request.headers) { + delete event.request.headers.authorization; + delete event.request.headers.cookie; + delete event.request.headers['x-internal-key']; + delete event.request.headers['x-vyndr-internal-key']; + } + } + return event; + }, + }); + _initialized = true; + return Sentry; +} + +function getSentry() { + return Sentry; +} + +function isInitialized() { + return _initialized; +} + +// Test helper — reset to the noop client so subsequent initSentry() +// calls re-run. Not exported via the main surface; live behind __internals. +function __resetForTests() { + _initialized = false; + Sentry = buildNoop(); +} + +module.exports = { + initSentry, + getSentry, + isInitialized, + // Convenience re-export so callers can do `const { Sentry } = require(...)`. + // Note: this is a live binding — mutated by initSentry() on first call. + get Sentry() { return Sentry; }, + __internals: { __resetForTests, buildNoop, buildClient }, +}; diff --git a/tests/unit/internalAuth.test.js b/tests/unit/internalAuth.test.js new file mode 100644 index 0000000..b08ffe7 --- /dev/null +++ b/tests/unit/internalAuth.test.js @@ -0,0 +1,160 @@ +// internalAuth middleware (Session 10) — tests both the new short-form +// header (x-internal-key) and the legacy long form +// (X-VYNDR-Internal-Key), the timing-safe compare, and the optional +// loopback restriction. + +const { requireInternalAuth, __internals } = require('../../src/middleware/internalAuth'); + +function fakeReqRes({ headers = {}, ip = '127.0.0.1' } = {}) { + const req = { + headers: Object.fromEntries(Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v])), + ip, + socket: { remoteAddress: ip }, + get(name) { + return this.headers[name.toLowerCase()]; + }, + }; + const res = { + statusCode: null, + body: null, + status(code) { this.statusCode = code; return this; }, + json(body) { this.body = body; return this; }, + }; + return { req, res }; +} + +function run(mw, req, res) { + return new Promise((resolve) => { + mw(req, res, () => resolve('next')); + if (res.statusCode != null) resolve('blocked'); + }); +} + +beforeEach(() => { + delete process.env.VYNDR_INTERNAL_KEY; +}); + +describe('requireInternalAuth', () => { + describe('configuration guard', () => { + test('returns 503 when VYNDR_INTERNAL_KEY env var is unset', async () => { + const mw = requireInternalAuth(); + const { req, res } = fakeReqRes({ headers: { 'x-internal-key': 'anything' } }); + const out = await run(mw, req, res); + expect(out).toBe('blocked'); + expect(res.statusCode).toBe(503); + expect(res.body.error).toMatch(/not configured/); + }); + }); + + describe('header compatibility', () => { + beforeEach(() => { process.env.VYNDR_INTERNAL_KEY = 'real-key-12345'; }); + + test('accepts the new short-form header (x-internal-key)', async () => { + const mw = requireInternalAuth(); + const { req, res } = fakeReqRes({ headers: { 'x-internal-key': 'real-key-12345' } }); + const out = await run(mw, req, res); + expect(out).toBe('next'); + }); + + test('accepts the legacy long-form header (X-VYNDR-Internal-Key)', async () => { + const mw = requireInternalAuth(); + const { req, res } = fakeReqRes({ headers: { 'x-vyndr-internal-key': 'real-key-12345' } }); + const out = await run(mw, req, res); + expect(out).toBe('next'); + }); + + test('returns 401 on missing header', async () => { + const mw = requireInternalAuth(); + const { req, res } = fakeReqRes({}); + const out = await run(mw, req, res); + expect(out).toBe('blocked'); + expect(res.statusCode).toBe(401); + }); + + test('returns 401 on wrong key (timing-safe)', async () => { + const mw = requireInternalAuth(); + const { req, res } = fakeReqRes({ headers: { 'x-internal-key': 'wrong-key' } }); + const out = await run(mw, req, res); + expect(out).toBe('blocked'); + expect(res.statusCode).toBe(401); + }); + + test('returns 401 even when prefix matches but suffix differs', async () => { + // Guards against early-exit string compare leaking timing info. + const mw = requireInternalAuth(); + const { req, res } = fakeReqRes({ headers: { 'x-internal-key': 'real-key-1234X' } }); + const out = await run(mw, req, res); + expect(out).toBe('blocked'); + expect(res.statusCode).toBe(401); + }); + + test('returns 401 on length mismatch (avoids any timingSafeEqual throw)', async () => { + const mw = requireInternalAuth(); + const { req, res } = fakeReqRes({ headers: { 'x-internal-key': 'short' } }); + const out = await run(mw, req, res); + expect(out).toBe('blocked'); + expect(res.statusCode).toBe(401); + }); + }); + + describe('loopbackOnly option', () => { + beforeEach(() => { process.env.VYNDR_INTERNAL_KEY = 'real-key-12345'; }); + + test('passes from 127.0.0.1', async () => { + const mw = requireInternalAuth({ loopbackOnly: true }); + const { req, res } = fakeReqRes({ headers: { 'x-internal-key': 'real-key-12345' }, ip: '127.0.0.1' }); + const out = await run(mw, req, res); + expect(out).toBe('next'); + }); + + test('passes from IPv6 loopback', async () => { + const mw = requireInternalAuth({ loopbackOnly: true }); + const { req, res } = fakeReqRes({ headers: { 'x-internal-key': 'real-key-12345' }, ip: '::1' }); + const out = await run(mw, req, res); + expect(out).toBe('next'); + }); + + test('passes from IPv4-mapped IPv6 loopback', async () => { + const mw = requireInternalAuth({ loopbackOnly: true }); + const { req, res } = fakeReqRes({ headers: { 'x-internal-key': 'real-key-12345' }, ip: '::ffff:127.0.0.1' }); + const out = await run(mw, req, res); + expect(out).toBe('next'); + }); + + test('returns 403 from off-host IP', async () => { + const mw = requireInternalAuth({ loopbackOnly: true }); + const { req, res } = fakeReqRes({ headers: { 'x-internal-key': 'real-key-12345' }, ip: '10.0.0.42' }); + const out = await run(mw, req, res); + expect(out).toBe('blocked'); + expect(res.statusCode).toBe(403); + expect(res.body.error).toMatch(/Origin not permitted/); + }); + + test('WITHOUT loopbackOnly, off-host IPs are accepted (n8n case)', async () => { + const mw = requireInternalAuth({ loopbackOnly: false }); + const { req, res } = fakeReqRes({ headers: { 'x-internal-key': 'real-key-12345' }, ip: '172.18.0.3' }); + const out = await run(mw, req, res); + expect(out).toBe('next'); + }); + }); + + describe('timingSafeStringEqual', () => { + const { timingSafeStringEqual } = __internals; + + test('equal strings → true', () => { + expect(timingSafeStringEqual('abc123', 'abc123')).toBe(true); + }); + test('different strings same length → false', () => { + expect(timingSafeStringEqual('abc123', 'abc124')).toBe(false); + }); + test('different lengths → false (does not throw)', () => { + expect(() => timingSafeStringEqual('a', 'abc')).not.toThrow(); + expect(timingSafeStringEqual('a', 'abc')).toBe(false); + }); + test('non-string inputs → false', () => { + expect(timingSafeStringEqual(null, 'a')).toBe(false); + expect(timingSafeStringEqual('a', undefined)).toBe(false); + expect(timingSafeStringEqual(123, '123')).toBe(false); + }); + }); +}); diff --git a/tests/unit/sentry.test.js b/tests/unit/sentry.test.js new file mode 100644 index 0000000..ad0e7ee --- /dev/null +++ b/tests/unit/sentry.test.js @@ -0,0 +1,132 @@ +// Sentry init wrapper — guarantees graceful no-op when SENTRY_DSN is +// absent, scrubs PII on send when configured, and exposes a usable +// surface to the rest of the codebase regardless of init state. + +// Mock @sentry/node BEFORE requiring our wrapper so we control what +// init() and setupExpressErrorHandler do. +const mockSentryInit = jest.fn(); +const mockSentryCapture = jest.fn(); +const mockSentryHandler = jest.fn(); +jest.mock('@sentry/node', () => ({ + init: (...a) => mockSentryInit(...a), + captureException: (...a) => mockSentryCapture(...a), + captureMessage: jest.fn(), + setupExpressErrorHandler: (...a) => mockSentryHandler(...a), + addBreadcrumb: jest.fn(), + setUser: jest.fn(), + setTag: jest.fn(), + setContext: jest.fn(), +})); + +const sentryWrapper = require('../../src/utils/sentry'); + +beforeEach(() => { + mockSentryInit.mockReset(); + mockSentryCapture.mockReset(); + mockSentryHandler.mockReset(); + sentryWrapper.__internals.__resetForTests(); + delete process.env.SENTRY_DSN; +}); + +describe('initSentry', () => { + test('no-op when SENTRY_DSN is unset (sentry.init NOT called)', () => { + sentryWrapper.initSentry(); + expect(mockSentryInit).not.toHaveBeenCalled(); + expect(sentryWrapper.isInitialized()).toBe(false); + }); + + test('initializes when SENTRY_DSN is set', () => { + process.env.SENTRY_DSN = 'https://abc@sentry.io/123'; + sentryWrapper.initSentry(); + expect(mockSentryInit).toHaveBeenCalledTimes(1); + expect(sentryWrapper.isInitialized()).toBe(true); + const cfg = mockSentryInit.mock.calls[0][0]; + expect(cfg.dsn).toBe('https://abc@sentry.io/123'); + expect(cfg.tracesSampleRate).toBe(0.1); + expect(cfg.sendDefaultPii).toBe(false); + expect(typeof cfg.beforeSend).toBe('function'); + }); + + test('explicit dsn arg overrides env', () => { + process.env.SENTRY_DSN = 'env-dsn'; + sentryWrapper.initSentry({ dsn: 'arg-dsn' }); + expect(mockSentryInit.mock.calls[0][0].dsn).toBe('arg-dsn'); + }); + + test('idempotent — second call is a no-op', () => { + process.env.SENTRY_DSN = 'https://abc@sentry.io/123'; + sentryWrapper.initSentry(); + sentryWrapper.initSentry(); + expect(mockSentryInit).toHaveBeenCalledTimes(1); + }); +}); + +describe('beforeSend PII scrubbing', () => { + beforeEach(() => { + process.env.SENTRY_DSN = 'https://abc@sentry.io/123'; + sentryWrapper.initSentry(); + }); + + test('strips user.ip_address and user.email', () => { + const cfg = mockSentryInit.mock.calls[0][0]; + const event = { user: { id: 'u1', ip_address: '1.2.3.4', email: 'a@b.com' } }; + const scrubbed = cfg.beforeSend(event); + expect(scrubbed.user.id).toBe('u1'); + expect(scrubbed.user.ip_address).toBeUndefined(); + expect(scrubbed.user.email).toBeUndefined(); + }); + + test('strips cookies + authorization + internal-key headers from request', () => { + const cfg = mockSentryInit.mock.calls[0][0]; + const event = { + request: { + cookies: { session: 'abc' }, + headers: { + authorization: 'Bearer secret', + cookie: 'session=abc', + 'x-internal-key': 'leak', + 'x-vyndr-internal-key': 'leak', + 'content-type': 'application/json', + }, + }, + }; + const scrubbed = cfg.beforeSend(event); + expect(scrubbed.request.cookies).toBeUndefined(); + expect(scrubbed.request.headers.authorization).toBeUndefined(); + expect(scrubbed.request.headers.cookie).toBeUndefined(); + expect(scrubbed.request.headers['x-internal-key']).toBeUndefined(); + expect(scrubbed.request.headers['x-vyndr-internal-key']).toBeUndefined(); + // Non-sensitive headers are kept. + expect(scrubbed.request.headers['content-type']).toBe('application/json'); + }); +}); + +describe('noop client surface', () => { + test('captureException is callable without DSN (no throw)', () => { + sentryWrapper.initSentry(); + expect(() => sentryWrapper.Sentry.captureException(new Error('test'))).not.toThrow(); + expect(mockSentryCapture).not.toHaveBeenCalled(); + }); + + test('setupExpressErrorHandler is callable without DSN (no throw)', () => { + sentryWrapper.initSentry(); + expect(() => sentryWrapper.Sentry.setupExpressErrorHandler({})).not.toThrow(); + expect(mockSentryHandler).not.toHaveBeenCalled(); + }); + + test('after init, captureException routes to real Sentry', () => { + process.env.SENTRY_DSN = 'https://abc@sentry.io/123'; + sentryWrapper.initSentry(); + const err = new Error('boom'); + sentryWrapper.Sentry.captureException(err); + expect(mockSentryCapture).toHaveBeenCalledWith(err); + }); + + test('after init, setupExpressErrorHandler delegates to real Sentry', () => { + process.env.SENTRY_DSN = 'https://abc@sentry.io/123'; + sentryWrapper.initSentry(); + const app = {}; + sentryWrapper.Sentry.setupExpressErrorHandler(app); + expect(mockSentryHandler).toHaveBeenCalledWith(app); + }); +}); diff --git a/tests/unit/soccerDataPrefetchCascade.test.js b/tests/unit/soccerDataPrefetchCascade.test.js new file mode 100644 index 0000000..e82d382 --- /dev/null +++ b/tests/unit/soccerDataPrefetchCascade.test.js @@ -0,0 +1,250 @@ +// Session 10 — prefetch's new api-football enrichment pass and the +// CLI flags (--source, --max-players). The existing soccerDataPrefetch +// test suite covers the legacy football-data path; this suite verifies: +// - parseArgs handles the new flags +// - shouldRunSource respects the source filter + defaults to 'all' +// - enrichFromApiFootball walks finished fixtures, aggregates per-90 +// rates, and writes apifootball:player_by_name:{normalizedName} +// - graceful skip when API_FOOTBALL_KEY is unset + +const mockApifGetFixtures = jest.fn(); +const mockApifGetFixturePlayerStats = jest.fn(); +const mockApifHasApiKey = jest.fn(() => true); +jest.mock('../../src/services/adapters/apiFootballAdapter', () => ({ + getFixtures: (...a) => mockApifGetFixtures(...a), + getFixturePlayerStats: (...a) => mockApifGetFixturePlayerStats(...a), + hasApiKey: (...a) => mockApifHasApiKey(...a), +})); + +const mockFootapiHasApiKey = jest.fn(() => false); +const mockFootapiGetRefereeStatistics = jest.fn(); +jest.mock('../../src/services/adapters/footApiAdapter', () => ({ + hasApiKey: (...a) => mockFootapiHasApiKey(...a), + getRefereeStatistics: (...a) => mockFootapiGetRefereeStatistics(...a), +})); + +const mockFbdHasApiKey = jest.fn(() => true); +const mockFbdGetLeagueStandings = jest.fn(async () => []); +const mockFbdGetLeagueScorers = jest.fn(async () => []); +jest.mock('../../src/services/adapters/footballDataAdapter', () => ({ + hasApiKey: (...a) => mockFbdHasApiKey(...a), + getLeagueStandings: (...a) => mockFbdGetLeagueStandings(...a), + getLeagueScorers: (...a) => mockFbdGetLeagueScorers(...a), +})); + +const mockCacheSets = new Map(); +jest.mock('../../src/utils/redis', () => ({ + cacheGet: async () => null, + cacheSet: async (k, v, ttl) => { mockCacheSets.set(k, { value: v, ttl }); return true; }, + cacheDel: async () => true, + isDegraded: () => false, +})); + +const { normalizeName } = require('../../src/utils/normalize'); +const prefetch = require('../../scripts/soccer-data-prefetch'); + +beforeEach(() => { + mockApifGetFixtures.mockReset(); + mockApifGetFixturePlayerStats.mockReset(); + mockApifHasApiKey.mockReset().mockReturnValue(true); + mockFbdHasApiKey.mockReset().mockReturnValue(true); + mockFbdGetLeagueStandings.mockReset().mockResolvedValue([]); + mockFbdGetLeagueScorers.mockReset().mockResolvedValue([]); + mockFootapiHasApiKey.mockReset().mockReturnValue(false); + mockFootapiGetRefereeStatistics.mockReset(); + mockCacheSets.clear(); +}); + +describe('soccer-data-prefetch — Session 10 cascade enrichment', () => { + describe('parseArgs', () => { + test('parses --source flag with valid value', () => { + const a = prefetch.__internals.parseArgs(['node', 'script', '--source=api-football']); + expect(a.source).toBe('api-football'); + }); + test('invalid --source falls back to "all"', () => { + const a = prefetch.__internals.parseArgs(['node', 'script', '--source=bogus']); + expect(a.source).toBe('all'); + }); + test('parses --max-players', () => { + const a = prefetch.__internals.parseArgs(['node', 'script', '--max-players=25']); + expect(a.maxPlayers).toBe(25); + }); + test('--max-players ignores non-numeric and zero/negative', () => { + expect(prefetch.__internals.parseArgs(['node', 'script', '--max-players=foo']).maxPlayers).toBe(80); + expect(prefetch.__internals.parseArgs(['node', 'script', '--max-players=0']).maxPlayers).toBe(80); + expect(prefetch.__internals.parseArgs(['node', 'script', '--max-players=-5']).maxPlayers).toBe(80); + }); + test('parses --season', () => { + expect(prefetch.__internals.parseArgs(['node', 'script', '--season=2025']).season).toBe(2025); + }); + test('defaults', () => { + const a = prefetch.__internals.parseArgs(['node', 'script']); + expect(a.source).toBe('all'); + expect(a.maxPlayers).toBe(80); + expect(a.season).toBe(2026); + }); + }); + + describe('shouldRunSource', () => { + const { shouldRunSource } = prefetch.__internals; + test('"all" matches everything', () => { + expect(shouldRunSource({ source: 'all' }, 'api-football')).toBe(true); + expect(shouldRunSource({ source: 'all' }, 'football-data')).toBe(true); + expect(shouldRunSource({ source: 'all' }, 'footapi')).toBe(true); + }); + test('explicit source matches only itself', () => { + expect(shouldRunSource({ source: 'api-football' }, 'api-football')).toBe(true); + expect(shouldRunSource({ source: 'api-football' }, 'football-data')).toBe(false); + }); + test('missing args.source defaults to "all" (backwards compat)', () => { + expect(shouldRunSource({}, 'football-data')).toBe(true); + expect(shouldRunSource(undefined, 'api-football')).toBe(true); + }); + }); + + describe('enrichFromApiFootball', () => { + test('graceful skip when API_FOOTBALL_KEY is unset', async () => { + mockApifHasApiKey.mockReturnValueOnce(false); + const r = await prefetch.__internals.enrichFromApiFootball('WC', { + season: 2026, maxPlayers: 80, dryRun: false, + }); + expect(r.skipped).toBe('no_key'); + expect(r.players).toBe(0); + expect(mockApifGetFixtures).not.toHaveBeenCalled(); + }); + + test('skips an unmapped league code (no api-football league ID)', async () => { + const r = await prefetch.__internals.enrichFromApiFootball('UNKNOWN', { + season: 2026, maxPlayers: 80, dryRun: false, + }); + expect(r.skipped).toBe('unmapped_league'); + expect(mockApifGetFixtures).not.toHaveBeenCalled(); + }); + + test('skips when no fixtures returned', async () => { + mockApifGetFixtures.mockResolvedValueOnce([]); + const r = await prefetch.__internals.enrichFromApiFootball('WC', { + season: 2026, maxPlayers: 80, dryRun: false, + }); + expect(r.skipped).toBe('no_fixtures'); + }); + + test('aggregates per-90 stats across finished fixtures, writes cascade keys', async () => { + mockApifGetFixtures.mockResolvedValueOnce([ + { id: 9001, status: 'FT' }, + { id: 9002, status: 'NS' }, // not yet played — skip + { id: 9003, status: 'FT' }, + ]); + // Fixture 9001: Messi 90min, 1G, 2A, 5 shots. + mockApifGetFixturePlayerStats.mockResolvedValueOnce([ + { name: 'Lionel Messi', team: 'Argentina', position: 'F', minutes: 90, goals: 1, assists: 2, shots_total: 5, shots_on: 3, substitute: false, rating: '8.4' }, + ]); + // Fixture 9003: Messi 88min, 0G, 1A. + mockApifGetFixturePlayerStats.mockResolvedValueOnce([ + { name: 'Lionel Messi', team: 'Argentina', position: 'F', minutes: 88, goals: 0, assists: 1, shots_total: 3, shots_on: 1, substitute: false, rating: '7.5' }, + ]); + + const r = await prefetch.__internals.enrichFromApiFootball('WC', { + season: 2026, maxPlayers: 80, dryRun: false, + }); + expect(r.players).toBe(1); + + const key = `apifootball:player_by_name:${normalizeName('Lionel Messi')}`; + const entry = mockCacheSets.get(key); + expect(entry).toBeDefined(); + expect(entry.value).toMatchObject({ + name: 'Lionel Messi', team: 'Argentina', + appearances: 2, + minutes: 178, // 90 + 88 + goals: 1, assists: 3, + }); + // 1 goal over 178 minutes = 0.506 per 90 (3dp). + expect(entry.value.goals_per_90).toBeCloseTo(0.506, 2); + // 3 assists over 178 min = 1.517 per 90. + expect(entry.value.assists_per_90).toBeCloseTo(1.517, 2); + // Average rating across two appearances. + expect(entry.value.avg_rating).toBeCloseTo(7.95, 1); + expect(entry.ttl).toBe(prefetch.__internals.PLAYER_TTL_SEC); + }); + + test('honors maxPlayers cap (limits writes per run)', async () => { + mockApifGetFixtures.mockResolvedValueOnce([{ id: 1, status: 'FT' }]); + mockApifGetFixturePlayerStats.mockResolvedValueOnce([ + { name: 'A', team: 'X', minutes: 90, goals: 0, assists: 0 }, + { name: 'B', team: 'X', minutes: 90, goals: 0, assists: 0 }, + { name: 'C', team: 'X', minutes: 90, goals: 0, assists: 0 }, + ]); + const r = await prefetch.__internals.enrichFromApiFootball('WC', { + season: 2026, maxPlayers: 2, dryRun: false, + }); + expect(r.players).toBe(2); + }); + + test('dry-run skips cacheSet but still reports a count', async () => { + mockApifGetFixtures.mockResolvedValueOnce([{ id: 1, status: 'FT' }]); + mockApifGetFixturePlayerStats.mockResolvedValueOnce([ + { name: 'A', team: 'X', minutes: 90, goals: 1, assists: 0 }, + ]); + const r = await prefetch.__internals.enrichFromApiFootball('WC', { + season: 2026, maxPlayers: 80, dryRun: true, + }); + expect(r.players).toBe(1); + expect(mockCacheSets.size).toBe(0); + }); + }); + + describe('enrichRefereesFromFootApi', () => { + test('graceful skip when RAPID_API_KEY is unset', async () => { + mockFootapiHasApiKey.mockReturnValueOnce(false); + const r = await prefetch.__internals.enrichRefereesFromFootApi( + [{ id: 1, name: 'X' }], { dryRun: false }, + ); + expect(r.skipped).toBe('no_key'); + expect(mockFootapiGetRefereeStatistics).not.toHaveBeenCalled(); + }); + + test('writes footapi:referee_by_name:{name} keys when stats exist', async () => { + mockFootapiHasApiKey.mockReturnValue(true); + mockFootapiGetRefereeStatistics.mockResolvedValueOnce([ + { tournamentId: 16, appearances: 6, yellowCards: 24, redCards: 1, yellowCardsPerGame: 4 }, + ]); + const r = await prefetch.__internals.enrichRefereesFromFootApi( + [{ id: 99, name: 'Anthony Taylor' }], { dryRun: false }, + ); + expect(r.referees).toBe(1); + const entry = mockCacheSets.get('footapi:referee_by_name:Anthony Taylor'); + expect(entry.value).toMatchObject({ + name: 'Anthony Taylor', cards_per_game: 4, appearances: 6, + }); + expect(entry.ttl).toBe(prefetch.__internals.REFEREE_TTL_SEC); + }); + + test('handles missing IDs in the referee list', async () => { + mockFootapiHasApiKey.mockReturnValue(true); + const r = await prefetch.__internals.enrichRefereesFromFootApi( + [{ id: null, name: 'X' }, { id: 1, name: null }], { dryRun: false }, + ); + expect(r.referees).toBe(0); + expect(mockFootapiGetRefereeStatistics).not.toHaveBeenCalled(); + }); + }); + + describe('main — graceful skip when no source keys configured', () => { + test('logs skip + returns {skipped: true} when nothing is available', async () => { + mockFbdHasApiKey.mockReturnValue(false); + mockApifHasApiKey.mockReturnValue(false); + mockFootapiHasApiKey.mockReturnValue(false); + const r = await prefetch.main(['node', 'script']); + expect(r.skipped).toBe(true); + }); + + test('proceeds when ANY source is available (api-football only)', async () => { + mockFbdHasApiKey.mockReturnValue(false); + mockApifHasApiKey.mockReturnValue(true); + mockApifGetFixtures.mockResolvedValue([]); + const r = await prefetch.main(['node', 'script', '--leagues=WC', '--dry-run']); + // Not skipped — we have at least one configured source. + expect(r.skipped).toBeUndefined(); + }); + }); +}); diff --git a/web/package-lock.json b/web/package-lock.json index fb12c24..b4c6969 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "UNLICENSED", "dependencies": { + "@sentry/nextjs": "^10.57.0", "@serwist/next": "^9.5.11", "@supabase/supabase-js": "2.99.3", "@tailwindcss/postcss": "4.2.2", @@ -42,12 +43,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -55,15 +56,243 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -579,6 +808,17 @@ "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==", + "license": "MIT", + "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", @@ -848,6 +1088,35 @@ "@opentelemetry/api": "^1.3.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==", + "license": "Apache-2.0", + "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/instrumentation/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==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@opentelemetry/otlp-exporter-base": { "version": "0.208.0", "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.208.0.tgz", @@ -902,12 +1171,12 @@ } }, "node_modules/@opentelemetry/resources": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.1.tgz", - "integrity": "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.1.tgz", + "integrity": "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", + "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -918,9 +1187,9 @@ } }, "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/core": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.1.tgz", - "integrity": "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.7.1.tgz", + "integrity": "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -1122,6 +1391,855 @@ "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", "license": "BSD-3-Clause" }, + "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==", + "license": "MIT", + "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/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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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/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==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz", + "integrity": "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz", + "integrity": "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz", + "integrity": "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz", + "integrity": "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz", + "integrity": "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz", + "integrity": "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz", + "integrity": "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz", + "integrity": "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz", + "integrity": "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz", + "integrity": "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz", + "integrity": "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz", + "integrity": "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz", + "integrity": "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz", + "integrity": "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz", + "integrity": "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz", + "integrity": "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz", + "integrity": "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz", + "integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz", + "integrity": "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz", + "integrity": "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz", + "integrity": "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz", + "integrity": "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz", + "integrity": "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz", + "integrity": "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz", + "integrity": "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sentry-internal/browser-utils": { + "version": "10.57.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.57.0.tgz", + "integrity": "sha512-tXObp954rMTSYKlbftjVXHtNl4t/6ssks3jkqyzmKb+PDPWzabGQO7sWwqVuTjT8Kx/8A3FmriS1bGmqxiJy3A==", + "license": "MIT", + "dependencies": { + "@sentry/core": "10.57.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/feedback": { + "version": "10.57.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.57.0.tgz", + "integrity": "sha512-ZcF4QhkqGX3iiQSXB2N0N3Awp+j5iqnDRu6PA/qyLFrWqH5ZiiAAgu59OLD9E6XAdg6iFtLYw19MAMZVK8qNOQ==", + "license": "MIT", + "dependencies": { + "@sentry/core": "10.57.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/replay": { + "version": "10.57.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.57.0.tgz", + "integrity": "sha512-Wmnx/6ABynVH1iwuoNUqJNyjIUqsqoGML7qsyivBRKb5Wo2YQtPOQlQYfxfZSvWzGpcoSVdInkRjDssUQxQEQg==", + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "10.57.0", + "@sentry/core": "10.57.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/replay-canvas": { + "version": "10.57.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.57.0.tgz", + "integrity": "sha512-zsfa4JcfV0AEc9YhNxNabd5lSZL2Av84saAyexGAqcHs+67m9Gd0cGStOzMb/nCl7UAtmdP0aI+G7a3rcxxN/A==", + "license": "MIT", + "dependencies": { + "@sentry-internal/replay": "10.57.0", + "@sentry/core": "10.57.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/server-utils": { + "version": "10.57.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/server-utils/-/server-utils-10.57.0.tgz", + "integrity": "sha512-Qu8ETmX/ITzteG7Im46b9HOxKKzeaIeqNvftaIlFURu1RUQdHbtGerS7QOmXzwnhuqNGNeiCQYkduB798IfRqA==", + "license": "MIT", + "dependencies": { + "@sentry/core": "10.57.0" + }, + "engines": { + "node": ">=18" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@sentry/browser": { + "version": "10.57.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.57.0.tgz", + "integrity": "sha512-s36AQy/CKXTfyY9Z+qUhzNomntZXgfs0rbaK7q9ffnFkqcPwzE8qQtVs58y3Suut56u+AhwSztgQtERcuZ5VIA==", + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "10.57.0", + "@sentry-internal/feedback": "10.57.0", + "@sentry-internal/replay": "10.57.0", + "@sentry-internal/replay-canvas": "10.57.0", + "@sentry/core": "10.57.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==", + "license": "MIT", + "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/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, + "license": "FSL-1.1-MIT", + "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==", + "license": "FSL-1.1-MIT", + "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" + ], + "license": "FSL-1.1-MIT", + "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" + ], + "license": "FSL-1.1-MIT", + "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" + ], + "license": "FSL-1.1-MIT", + "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" + ], + "license": "FSL-1.1-MIT", + "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" + ], + "license": "FSL-1.1-MIT", + "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" + ], + "license": "FSL-1.1-MIT", + "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" + ], + "license": "FSL-1.1-MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/core": { + "version": "10.57.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.57.0.tgz", + "integrity": "sha512-kntItTA2kiT0YpL7encXaF6mkdZMB+y48lwj8w1wkfBpfJAC7sifdgrzLQZqmsqVNE3crg9VfufaAGA+78uFMg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/nextjs": { + "version": "10.57.0", + "resolved": "https://registry.npmjs.org/@sentry/nextjs/-/nextjs-10.57.0.tgz", + "integrity": "sha512-jRsyc387+YoOpYoxtJaL8VLzCq4I0KrIsVcZW4j3gA4LHG2nolKV4IDGrYWxEygc41Sgrtm1ZYbZAvjHMd9phQ==", + "license": "MIT", + "dependencies": { + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/semantic-conventions": "^1.40.0", + "@rollup/plugin-commonjs": "28.0.1", + "@sentry-internal/browser-utils": "10.57.0", + "@sentry/bundler-plugin-core": "^5.3.0", + "@sentry/core": "10.57.0", + "@sentry/node": "10.57.0", + "@sentry/opentelemetry": "10.57.0", + "@sentry/react": "10.57.0", + "@sentry/vercel-edge": "10.57.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.57.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-10.57.0.tgz", + "integrity": "sha512-7KEStrJ97wPf1fA5nU5ONeTTcIIlh7oT8OMffEVA1PXmlhFoXhcQZVzr4rM+zj9tfMWT01og5Ng/Grgh3dN+FA==", + "license": "MIT", + "dependencies": { + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/core": "^2.6.1", + "@opentelemetry/instrumentation": "^0.214.0", + "@opentelemetry/sdk-trace-base": "^2.6.1", + "@opentelemetry/semantic-conventions": "^1.40.0", + "@sentry-internal/server-utils": "10.57.0", + "@sentry/core": "10.57.0", + "@sentry/node-core": "10.57.0", + "@sentry/opentelemetry": "10.57.0", + "import-in-the-middle": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node-core": { + "version": "10.57.0", + "resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-10.57.0.tgz", + "integrity": "sha512-2v2IF6MfTiu7pimWEq2rYhZsmlwyNbs3bHUsrYFPeP/Rpa6ObDuUWPdVEzJjfyK+AqqYZYxZdV0l3+B13kTEmQ==", + "license": "MIT", + "dependencies": { + "@sentry/core": "10.57.0", + "@sentry/opentelemetry": "10.57.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", + "@opentelemetry/semantic-conventions": "^1.39.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 + }, + "@opentelemetry/semantic-conventions": { + "optional": true + } + } + }, + "node_modules/@sentry/node/node_modules/@opentelemetry/core": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.7.1.tgz", + "integrity": "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw==", + "license": "Apache-2.0", + "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/@sentry/node/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.1.tgz", + "integrity": "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.7.1", + "@opentelemetry/resources": "2.7.1", + "@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/@sentry/opentelemetry": { + "version": "10.57.0", + "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-10.57.0.tgz", + "integrity": "sha512-iwRz8cEK0GOISG34aJRO8GdYOk3nfpuT6dT2GDQrxw8f7JjkJKx9LPU8MaenOFa4MhY+Z02hI6NNcrbsoI3cXg==", + "license": "MIT", + "dependencies": { + "@sentry/core": "10.57.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", + "@opentelemetry/semantic-conventions": "^1.39.0" + } + }, + "node_modules/@sentry/react": { + "version": "10.57.0", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-10.57.0.tgz", + "integrity": "sha512-6QThwQ4XWQ2rwKZEVQ9P9WKl7JlowC7S5LpAvmMdrwlfJBpLDFOsM7tycnIvbXTXf0ZOOuLFPa4L4YYbdyNGmA==", + "license": "MIT", + "dependencies": { + "@sentry/browser": "10.57.0", + "@sentry/core": "10.57.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.14.0 || 17.x || 18.x || 19.x" + } + }, + "node_modules/@sentry/vercel-edge": { + "version": "10.57.0", + "resolved": "https://registry.npmjs.org/@sentry/vercel-edge/-/vercel-edge-10.57.0.tgz", + "integrity": "sha512-8liagiXIWfyG0xGMmZQPCNOvqfzJcL7djB2jjNCaF5y7C+X/NTUHr4sqUHfAHqpQFUUXqmBT/mZv9HB5FDLhyQ==", + "license": "MIT", + "dependencies": { + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/resources": "^2.6.1", + "@sentry/core": "10.57.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==", + "license": "MIT", + "dependencies": { + "@sentry/bundler-plugin-core": "5.3.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "webpack": ">=5.0.0" + } + }, "node_modules/@serwist/build": { "version": "9.5.11", "resolved": "https://registry.npmjs.org/@serwist/build/-/build-9.5.11.tgz", @@ -1610,9 +2728,9 @@ } }, "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==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "license": "MIT" }, "node_modules/@types/estree-jsx": { @@ -1633,6 +2751,13 @@ "@types/unist": "*" } }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT", + "peer": true + }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -1705,6 +2830,181 @@ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "license": "ISC" }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "Apache-2.0", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "BSD-3-Clause", + "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==", + "license": "Apache-2.0", + "peer": true + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -1717,6 +3017,28 @@ "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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", @@ -1739,6 +3061,66 @@ "node": ">=0.4.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==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "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==", + "license": "MIT", + "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": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "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==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -1833,6 +3215,13 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.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==", + "license": "MIT", + "peer": true + }, "node_modules/caniuse-lite": { "version": "1.0.30001797", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001797.tgz", @@ -1903,6 +3292,22 @@ "url": "https://github.com/sponsors/wooorm" } }, + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -1948,6 +3353,18 @@ "node": ">=4.0.0" } }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "license": "MIT" + }, + "node_modules/convert-source-map": { + "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==", + "license": "MIT" + }, "node_modules/core-js": { "version": "3.49.0", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", @@ -2042,6 +3459,18 @@ "@types/trusted-types": "^2.0.7" } }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -2056,18 +3485,25 @@ "license": "ISC" }, "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==", + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.23.0.tgz", + "integrity": "sha512-yJN/BOOLxcOW2aQgeif9mSnaUB8KtvmMMp56oA1kx1CRfBKbhZm2pJ+NBY+3eOboHxix8lfjWpHE0Ei5U8RbSA==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.3.0" + "tapable": "^2.3.3" }, "engines": { "node": ">=10.13.0" } }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "license": "MIT", + "peer": true + }, "node_modules/esast-util-from-estree": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", @@ -2122,6 +3558,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -2135,6 +3585,39 @@ "node": ">=4" } }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, "node_modules/estree-util-attach-comments": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", @@ -2226,6 +3709,16 @@ "@types/estree": "^1.0.0" } }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -2244,12 +3737,92 @@ "node": ">=0.10.0" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT", + "peer": true + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/fflate": { "version": "0.4.8", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz", "integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==", "license": "MIT" }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "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==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/glob": { "version": "13.0.6", "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", @@ -2267,6 +3840,13 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "license": "BSD-2-Clause", + "peer": true + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -2304,6 +3884,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/hast-util-to-estree": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz", @@ -2379,6 +3969,19 @@ "dev": true, "license": "MIT" }, + "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==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/iceberg-js": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", @@ -2394,6 +3997,21 @@ "integrity": "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==", "license": "ISC" }, + "node_modules/import-in-the-middle": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.0.2.tgz", + "integrity": "sha512-LGLYRl0A2gtyUJb2WDliBHmk6TtlHwdDjxonacZ8QrEs/ZW+YDgNv2QAfjRQWpS8HqvNcq6GGnN6jrOa5FysDQ==", + "license": "Apache-2.0", + "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/inline-style-parser": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", @@ -2475,6 +4093,36 @@ "node": ">=0.10.0" } }, + "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==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "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==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -2503,6 +4151,37 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "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==", + "license": "MIT", + "peer": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -2767,6 +4446,35 @@ "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -2989,6 +4697,13 @@ "url": "https://opencollective.com/unified" } }, + "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==", + "license": "MIT", + "peer": true + }, "node_modules/micromark": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", @@ -3585,6 +5300,16 @@ ], "license": "MIT" }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", @@ -3609,6 +5334,12 @@ "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==", + "license": "MIT" + }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -3643,6 +5374,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "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==", + "license": "MIT", + "peer": true + }, "node_modules/next": { "version": "16.2.6", "resolved": "https://registry.npmjs.org/next/-/next-16.2.6.tgz", @@ -3746,6 +5484,48 @@ "node": "^10 || ^12 || >=14" } }, + "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==", + "license": "MIT", + "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-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/node-fetch/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==", + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/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==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/node-releases": { "version": "2.0.47", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", @@ -3765,6 +5545,36 @@ "opener": "bin/opener-bin.js" } }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "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==", + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parse-entities": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", @@ -3790,6 +5600,15 @@ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", "license": "MIT" }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-scurry": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", @@ -3812,6 +5631,18 @@ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/postcss": { "version": "8.5.14", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", @@ -3883,6 +5714,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/property-information": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", @@ -3917,6 +5757,12 @@ "node": ">=12.0.0" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -4082,12 +5928,99 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "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==", + "license": "MIT", + "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/rollup": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.1.tgz", + "integrity": "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==", + "license": "MIT", + "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.61.1", + "@rollup/rollup-android-arm64": "4.61.1", + "@rollup/rollup-darwin-arm64": "4.61.1", + "@rollup/rollup-darwin-x64": "4.61.1", + "@rollup/rollup-freebsd-arm64": "4.61.1", + "@rollup/rollup-freebsd-x64": "4.61.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", + "@rollup/rollup-linux-arm-musleabihf": "4.61.1", + "@rollup/rollup-linux-arm64-gnu": "4.61.1", + "@rollup/rollup-linux-arm64-musl": "4.61.1", + "@rollup/rollup-linux-loong64-gnu": "4.61.1", + "@rollup/rollup-linux-loong64-musl": "4.61.1", + "@rollup/rollup-linux-ppc64-gnu": "4.61.1", + "@rollup/rollup-linux-ppc64-musl": "4.61.1", + "@rollup/rollup-linux-riscv64-gnu": "4.61.1", + "@rollup/rollup-linux-riscv64-musl": "4.61.1", + "@rollup/rollup-linux-s390x-gnu": "4.61.1", + "@rollup/rollup-linux-x64-gnu": "4.61.1", + "@rollup/rollup-linux-x64-musl": "4.61.1", + "@rollup/rollup-openbsd-x64": "4.61.1", + "@rollup/rollup-openharmony-arm64": "4.61.1", + "@rollup/rollup-win32-arm64-msvc": "4.61.1", + "@rollup/rollup-win32-ia32-msvc": "4.61.1", + "@rollup/rollup-win32-x64-gnu": "4.61.1", + "@rollup/rollup-win32-x64-msvc": "4.61.1", + "fsevents": "~2.3.2" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", "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==", + "license": "MIT", + "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/section-matter": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", @@ -4209,6 +6142,27 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "peer": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/space-separated-tokens": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", @@ -4225,6 +6179,27 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "license": "BSD-3-Clause" }, + "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==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.7.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/stacktrace-parser/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==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, "node_modules/stringify-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", @@ -4289,6 +6264,22 @@ } } }, + "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==", + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/tagged-tag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", @@ -4308,9 +6299,9 @@ "license": "MIT" }, "node_modules/tapable": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", "license": "MIT", "engines": { "node": ">=6" @@ -4320,6 +6311,93 @@ "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==", + "license": "BSD-2-Clause", + "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/terser-webpack-plugin": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==", + "license": "MIT", + "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/terser/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==", + "license": "MIT", + "peer": true + }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -4587,6 +6665,20 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "license": "MIT", + "peer": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/web-vitals": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.2.0.tgz", @@ -4599,6 +6691,53 @@ "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", "license": "BSD-2-Clause" }, + "node_modules/webpack": { + "version": "5.107.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.107.2.tgz", + "integrity": "sha512-v7RhXaJbpMlV0D7hC7lb2EbnxkoeUqf9qhKr6lozx3Q48pmFrqqNRmZFUEGmi7pSwm6fCQ2H1IjvCkHqdpVdjQ==", + "license": "MIT", + "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.0", + "es-module-lexer": "^2.1.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "loader-runner": "^4.3.2", + "mime-db": "^1.54.0", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.5.0", + "watchpack": "^2.5.1", + "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-bundle-analyzer": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.1.tgz", @@ -4649,6 +6788,16 @@ } } }, + "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==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/whatwg-url": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", @@ -4660,6 +6809,21 @@ "webidl-conversions": "^4.0.2" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/ws": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", @@ -4681,6 +6845,12 @@ } } }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, "node_modules/yaml": { "version": "2.8.3", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", @@ -4696,6 +6866,18 @@ "url": "https://github.com/sponsors/eemeli" } }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zod": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.1.tgz", diff --git a/web/package.json b/web/package.json index f695738..a5e4860 100644 --- a/web/package.json +++ b/web/package.json @@ -11,6 +11,7 @@ }, "license": "UNLICENSED", "dependencies": { + "@sentry/nextjs": "^10.57.0", "@serwist/next": "^9.5.11", "@supabase/supabase-js": "2.99.3", "@tailwindcss/postcss": "4.2.2", diff --git a/web/public/sw.js b/web/public/sw.js index 52b2ad2..a1a1eb2 100644 --- a/web/public/sw.js +++ b/web/public/sw.js @@ -1,2 +1,2 @@ (()=>{"use strict";let e,t,a,s,r,n={googleAnalytics:"googleAnalytics",precache:"precache-v2",prefix:"serwist",runtime:"runtime",suffix:"u">typeof registration?registration.scope:""},i=e=>[n.prefix,e,n.suffix].filter(e=>e&&e.length>0).join("-"),c=e=>e||i(n.precache),o=e=>e||i(n.runtime);var l=class extends Error{details;constructor(e,t){super(((e,...t)=>{let a=e;return t.length>0&&(a+=` :: ${JSON.stringify(t)}`),a})(e,t)),this.name=e,this.details=t}};function h(e){return new Promise(t=>setTimeout(t,e))}let u=new Set;function d(e,t){let a=new URL(e);for(let e of t)a.searchParams.delete(e);return a.href}async function m(e,t,a,s){let r=d(t.url,a);if(t.url===r)return e.match(t,s);let n={...s,ignoreSearch:!0};for(let i of(await e.keys(t,n)))if(r===d(i.url,a))return e.match(i,s)}var f=class{promise;resolve;reject;constructor(){this.promise=new Promise((e,t)=>{this.resolve=e,this.reject=t})}};let g=async()=>{for(let e of u)await e()},w="-precache-",p=async(e,t=w)=>{let a=(await self.caches.keys()).filter(a=>a.includes(t)&&a.includes(self.registration.scope)&&a!==e);return await Promise.all(a.map(e=>self.caches.delete(e))),a},y=(e,t)=>{let a=t();return e.waitUntil(a),a},_=(e,t)=>t.some(t=>e instanceof t),x=new WeakMap,b=new WeakMap,v=new WeakMap,E={get(e,t,a){if(e instanceof IDBTransaction){if("done"===t)return x.get(e);if("store"===t)return a.objectStoreNames[1]?void 0:a.objectStore(a.objectStoreNames[0])}return R(e[t])},set:(e,t,a)=>(e[t]=a,!0),has:(e,t)=>e instanceof IDBTransaction&&("done"===t||"store"===t)||t in e};function R(e){if(e instanceof IDBRequest){let t;return t=new Promise((t,a)=>{let s=()=>{e.removeEventListener("success",r),e.removeEventListener("error",n)},r=()=>{t(R(e.result)),s()},n=()=>{a(e.error),s()};e.addEventListener("success",r),e.addEventListener("error",n)}),v.set(t,e),t}if(b.has(e))return b.get(e);let t=function(e){if("function"==typeof e)return(r||(r=[IDBCursor.prototype.advance,IDBCursor.prototype.continue,IDBCursor.prototype.continuePrimaryKey])).includes(e)?function(...t){return e.apply(q(this),t),R(this.request)}:function(...t){return R(e.apply(q(this),t))};return(e instanceof IDBTransaction&&function(e){if(x.has(e))return;let t=new Promise((t,a)=>{let s=()=>{e.removeEventListener("complete",r),e.removeEventListener("error",n),e.removeEventListener("abort",n)},r=()=>{t(),s()},n=()=>{a(e.error||new DOMException("AbortError","AbortError")),s()};e.addEventListener("complete",r),e.addEventListener("error",n),e.addEventListener("abort",n)});x.set(e,t)}(e),_(e,s||(s=[IDBDatabase,IDBObjectStore,IDBIndex,IDBCursor,IDBTransaction])))?new Proxy(e,E):e}(e);return t!==e&&(b.set(e,t),v.set(t,e)),t}let q=e=>v.get(e);function S(e,t,{blocked:a,upgrade:s,blocking:r,terminated:n}={}){let i=indexedDB.open(e,t),c=R(i);return s&&i.addEventListener("upgradeneeded",e=>{s(R(i.result),e.oldVersion,e.newVersion,R(i.transaction),e)}),a&&i.addEventListener("blocked",e=>a(e.oldVersion,e.newVersion,e)),c.then(e=>{n&&e.addEventListener("close",()=>n()),r&&e.addEventListener("versionchange",e=>r(e.oldVersion,e.newVersion,e))}).catch(()=>{}),c}let D=["get","getKey","getAll","getAllKeys","count"],N=["put","add","delete","clear"],C=new Map;function T(e,t){if(!(e instanceof IDBDatabase&&!(t in e)&&"string"==typeof t))return;if(C.get(t))return C.get(t);let a=t.replace(/FromIndex$/,""),s=t!==a,r=N.includes(a);if(!(a in(s?IDBIndex:IDBObjectStore).prototype)||!(r||D.includes(a)))return;let n=async function(e,...t){let n=this.transaction(e,r?"readwrite":"readonly"),i=n.store;return s&&(i=i.index(t.shift())),(await Promise.all([i[a](...t),r&&n.done]))[0]};return C.set(t,n),n}E={...e=E,get:(t,a,s)=>T(t,a)||e.get(t,a,s),has:(t,a)=>!!T(t,a)||e.has(t,a)};let P=["continue","continuePrimaryKey","advance"],k={},A=new WeakMap,I=new WeakMap,U={get(e,t){if(!P.includes(t))return e[t];let a=k[t];return a||(a=k[t]=function(...e){A.set(this,I.get(this)[t](...e))}),a}};async function*L(...e){let t=this;if(t instanceof IDBCursor||(t=await t.openCursor(...e)),!t)return;let a=new Proxy(t,U);for(I.set(a,t),v.set(a,q(t));t;)yield a,t=await (A.get(a)||t.continue()),A.delete(a)}function F(e,t){return t===Symbol.asyncIterator&&_(e,[IDBIndex,IDBObjectStore,IDBCursor])||"iterate"===t&&_(e,[IDBIndex,IDBObjectStore])}E={...t=E,get:(e,a,s)=>F(e,a)?L:t.get(e,a,s),has:(e,a)=>F(e,a)||t.has(e,a)};let M=async(e,t)=>{let s=null;if(e.url&&(s=new URL(e.url).origin),s!==self.location.origin)throw new l("cross-origin-copy-response",{origin:s});let r=e.clone(),n={headers:new Headers(r.headers),status:r.status,statusText:r.statusText},i=t?t(n):n,c=!function(){if(void 0===a){let e=new Response("");if("body"in e)try{new Response(e.body),a=!0}catch{a=!1}a=!1}return a}()?await r.blob():r.body;return new Response(c,i)},O="requests",B="queueName";var K=class{_db=null;async addEntry(e){let t=(await this.getDb()).transaction(O,"readwrite",{durability:"relaxed"});await t.store.add(e),await t.done}async getFirstEntryId(){return(await (await this.getDb()).transaction(O).store.openCursor())?.value.id}async getAllEntriesByQueueName(e){return await (await this.getDb()).getAllFromIndex(O,B,IDBKeyRange.only(e))||[]}async getEntryCountByQueueName(e){return(await this.getDb()).countFromIndex(O,B,IDBKeyRange.only(e))}async deleteEntry(e){await (await this.getDb()).delete(O,e)}async getFirstEntryByQueueName(e){return await this.getEndEntryFromIndex(IDBKeyRange.only(e),"next")}async getLastEntryByQueueName(e){return await this.getEndEntryFromIndex(IDBKeyRange.only(e),"prev")}async getEndEntryFromIndex(e,t){return(await (await this.getDb()).transaction(O).store.index(B).openCursor(e,t))?.value}async getDb(){return this._db||(this._db=await S("serwist-background-sync",3,{upgrade:this._upgradeDb})),this._db}_upgradeDb(e,t){t>0&&t<3&&e.objectStoreNames.contains(O)&&e.deleteObjectStore(O),e.createObjectStore(O,{autoIncrement:!0,keyPath:"id"}).createIndex(B,B,{unique:!1})}},W=class{_queueName;_queueDb;constructor(e){this._queueName=e,this._queueDb=new K}async pushEntry(e){delete e.id,e.queueName=this._queueName,await this._queueDb.addEntry(e)}async unshiftEntry(e){let t=await this._queueDb.getFirstEntryId();t?e.id=t-1:delete e.id,e.queueName=this._queueName,await this._queueDb.addEntry(e)}async popEntry(){return this._removeEntry(await this._queueDb.getLastEntryByQueueName(this._queueName))}async shiftEntry(){return this._removeEntry(await this._queueDb.getFirstEntryByQueueName(this._queueName))}async getAll(){return await this._queueDb.getAllEntriesByQueueName(this._queueName)}async size(){return await this._queueDb.getEntryCountByQueueName(this._queueName)}async deleteEntry(e){await this._queueDb.deleteEntry(e)}async _removeEntry(e){return e&&await this.deleteEntry(e.id),e}};let j=["method","referrer","referrerPolicy","mode","credentials","cache","redirect","integrity","keepalive"];var $=class e{_requestData;static async fromRequest(t){let a={url:t.url,headers:{}};for(let e of("GET"!==t.method&&(a.body=await t.clone().arrayBuffer()),t.headers.forEach((e,t)=>{a.headers[t]=e}),j))void 0!==t[e]&&(a[e]=t[e]);return new e(a)}constructor(e){"navigate"===e.mode&&(e.mode="same-origin"),this._requestData=e}toObject(){let e=Object.assign({},this._requestData);return e.headers=Object.assign({},this._requestData.headers),e.body&&(e.body=e.body.slice(0)),e}toRequest(){return new Request(this._requestData.url,this._requestData)}clone(){return new e(this.toObject())}};let H="serwist-background-sync",V=new Set,G=e=>{let t={request:new $(e.requestData).toRequest(),timestamp:e.timestamp};return e.metadata&&(t.metadata=e.metadata),t};var Q=class{_name;_onSync;_maxRetentionTime;_queueStore;_forceSyncFallback;_syncInProgress=!1;_requestsAddedDuringSync=!1;constructor(e,{forceSyncFallback:t,onSync:a,maxRetentionTime:s}={}){if(V.has(e))throw new l("duplicate-queue-name",{name:e});V.add(e),this._name=e,this._onSync=a||this.replayRequests,this._maxRetentionTime=s||10080,this._forceSyncFallback=!!t,this._queueStore=new W(this._name),this._addSyncListener()}get name(){return this._name}async pushRequest(e){await this._addRequest(e,"push")}async unshiftRequest(e){await this._addRequest(e,"unshift")}async popRequest(){return this._removeRequest("pop")}async shiftRequest(){return this._removeRequest("shift")}async getAll(){let e=await this._queueStore.getAll(),t=Date.now(),a=[];for(let s of e){let e=60*this._maxRetentionTime*1e3;t-s.timestamp>e?await this._queueStore.deleteEntry(s.id):a.push(G(s))}return a}async size(){return await this._queueStore.size()}async _addRequest({request:e,metadata:t,timestamp:a=Date.now()},s){let r={requestData:(await $.fromRequest(e.clone())).toObject(),timestamp:a};switch(t&&(r.metadata=t),s){case"push":await this._queueStore.pushEntry(r);break;case"unshift":await this._queueStore.unshiftEntry(r)}this._syncInProgress?this._requestsAddedDuringSync=!0:await this.registerSync()}async _removeRequest(e){let t,a=Date.now();switch(e){case"pop":t=await this._queueStore.popEntry();break;case"shift":t=await this._queueStore.shiftEntry()}if(t){let s=60*this._maxRetentionTime*1e3;return a-t.timestamp>s?this._removeRequest(e):G(t)}}async replayRequests(){let e;for(;e=await this.shiftRequest();)try{await fetch(e.request.clone())}catch{throw await this.unshiftRequest(e),new l("queue-replay-failed",{name:this._name})}}async registerSync(){if("sync"in self.registration&&!this._forceSyncFallback)try{await self.registration.sync.register(`${H}:${this._name}`)}catch(e){}}_addSyncListener(){"sync"in self.registration&&!this._forceSyncFallback?self.addEventListener("sync",e=>{if(e.tag===`${H}:${this._name}`){let t=async()=>{let t;this._syncInProgress=!0;try{await this._onSync({queue:this})}catch(e){if(e instanceof Error)throw e}finally{this._requestsAddedDuringSync&&!(t&&!e.lastChance)&&await this.registerSync(),this._syncInProgress=!1,this._requestsAddedDuringSync=!1}};e.waitUntil(t())}}):this._onSync({queue:this})}static get _queueNames(){return V}},z=class{_queue;constructor(e,t){this._queue=new Q(e,t)}async fetchDidFail({request:e}){await this._queue.pushRequest({request:e})}};let Y={cacheWillUpdate:async({response:e})=>200===e.status||0===e.status?e:null};function J(e){return"string"==typeof e?new Request(e):e}var X=class{event;request;url;params;_cacheKeys={};_strategy;_handlerDeferred;_extendLifetimePromises;_plugins;_pluginStateMap;constructor(e,t){for(const a of(this.event=t.event,this.request=t.request,t.url&&(this.url=t.url,this.params=t.params),this._strategy=e,this._handlerDeferred=new f,this._extendLifetimePromises=[],this._plugins=[...e.plugins],this._pluginStateMap=new Map,this._plugins))this._pluginStateMap.set(a,{});this.event.waitUntil(this._handlerDeferred.promise)}async fetch(e){let{event:t}=this,a=J(e),s=await this.getPreloadResponse();if(s)return s;let r=this.hasCallback("fetchDidFail")?a.clone():null;try{for(let e of this.iterateCallbacks("requestWillFetch"))a=await e({request:a.clone(),event:t})}catch(e){if(e instanceof Error)throw new l("plugin-error-request-will-fetch",{thrownErrorMessage:e.message})}let n=a.clone();try{let e;for(let s of(e=await fetch(a,"navigate"===a.mode?void 0:this._strategy.fetchOptions),this.iterateCallbacks("fetchDidSucceed")))e=await s({event:t,request:n,response:e});return e}catch(e){throw r&&await this.runCallbacks("fetchDidFail",{error:e,event:t,originalRequest:r.clone(),request:n.clone()}),e}}async fetchAndCachePut(e){let t=await this.fetch(e),a=t.clone();return this.waitUntil(this.cachePut(e,a)),t}async cacheMatch(e){let t,a=J(e),{cacheName:s,matchOptions:r}=this._strategy,n=await this.getCacheKey(a,"read"),i={...r,cacheName:s};for(let e of(t=await caches.match(n,i),this.iterateCallbacks("cachedResponseWillBeUsed")))t=await e({cacheName:s,matchOptions:r,cachedResponse:t,request:n,event:this.event})||void 0;return t}async cachePut(e,t){let a=J(e);await h(0);let s=await this.getCacheKey(a,"write");if(!t)throw new l("cache-put-with-no-response",{url:new URL(String(s.url),location.href).href.replace(RegExp(`^${location.origin}`),"")});let r=await this._ensureResponseSafeToCache(t);if(!r)return!1;let{cacheName:n,matchOptions:i}=this._strategy,c=await self.caches.open(n),o=this.hasCallback("cacheDidUpdate"),u=o?await m(c,s.clone(),["__WB_REVISION__"],i):null;try{await c.put(s,o?r.clone():r)}catch(e){if(e instanceof Error)throw"QuotaExceededError"===e.name&&await g(),e}for(let e of this.iterateCallbacks("cacheDidUpdate"))await e({cacheName:n,oldResponse:u,newResponse:r.clone(),request:s,event:this.event});return!0}async getCacheKey(e,t){let a=`${e.url} | ${t}`;if(!this._cacheKeys[a]){let s=e;for(let e of this.iterateCallbacks("cacheKeyWillBeUsed"))s=J(await e({mode:t,request:s,event:this.event,params:this.params}));this._cacheKeys[a]=s}return this._cacheKeys[a]}hasCallback(e){for(let t of this._strategy.plugins)if(e in t)return!0;return!1}async runCallbacks(e,t){for(let a of this.iterateCallbacks(e))await a(t)}*iterateCallbacks(e){for(let t of this._strategy.plugins)if("function"==typeof t[e]){let a=this._pluginStateMap.get(t),s=s=>{let r={...s,state:a};return t[e](r)};yield s}}waitUntil(e){return this._extendLifetimePromises.push(e),e}async doneWaiting(){let e;for(;e=this._extendLifetimePromises.shift();)await e}destroy(){this._handlerDeferred.resolve(null)}async getPreloadResponse(){if(this.event instanceof FetchEvent&&"navigate"===this.event.request.mode&&"preloadResponse"in this.event)try{let e=await this.event.preloadResponse;if(e)return e}catch(e){return}}async _ensureResponseSafeToCache(e){let t=e,a=!1;for(let e of this.iterateCallbacks("cacheWillUpdate"))if(t=await e({request:this.request,response:t,event:this.event})||void 0,a=!0,!t)break;return!a&&t&&200!==t.status&&(t=void 0),t}},Z=class{cacheName;plugins;fetchOptions;matchOptions;constructor(e={}){this.cacheName=o(e.cacheName),this.plugins=e.plugins||[],this.fetchOptions=e.fetchOptions,this.matchOptions=e.matchOptions}handle(e){let[t]=this.handleAll(e);return t}handleAll(e){e instanceof FetchEvent&&(e={event:e,request:e.request});let t=e.event,a="string"==typeof e.request?new Request(e.request):e.request,s=new X(this,e.url?{event:t,request:a,url:e.url,params:e.params}:{event:t,request:a}),r=this._getResponse(s,a,t);return[r,this._awaitComplete(r,s,a,t)]}async _getResponse(e,t,a){let s;await e.runCallbacks("handlerWillStart",{event:a,request:t});try{if(s=await this._handle(t,e),void 0===s||"error"===s.type)throw new l("no-response",{url:t.url})}catch(r){if(r instanceof Error){for(let n of e.iterateCallbacks("handlerDidError"))if(void 0!==(s=await n({error:r,event:a,request:t})))break}if(!s)throw r}for(let r of e.iterateCallbacks("handlerWillRespond"))s=await r({event:a,request:t,response:s});return s}async _awaitComplete(e,t,a,s){let r,n;try{r=await e}catch{}try{await t.runCallbacks("handlerDidRespond",{event:s,request:a,response:r}),await t.doneWaiting()}catch(e){e instanceof Error&&(n=e)}if(await t.runCallbacks("handlerDidComplete",{event:s,request:a,response:r,error:n}),t.destroy(),n)throw n}},ee=class extends Z{_networkTimeoutSeconds;constructor(e={}){super(e),this.plugins.some(e=>"cacheWillUpdate"in e)||this.plugins.unshift(Y),this._networkTimeoutSeconds=e.networkTimeoutSeconds||0}async _handle(e,t){let a,s=[],r=[];if(this._networkTimeoutSeconds){let{id:n,promise:i}=this._getTimeoutPromise({request:e,logs:s,handler:t});a=n,r.push(i)}let n=this._getNetworkPromise({timeoutId:a,request:e,logs:s,handler:t});r.push(n);let i=await t.waitUntil((async()=>await t.waitUntil(Promise.race(r))||await n)());if(!i)throw new l("no-response",{url:e.url});return i}_getTimeoutPromise({request:e,logs:t,handler:a}){let s;return{promise:new Promise(t=>{s=setTimeout(async()=>{t(await a.cacheMatch(e))},1e3*this._networkTimeoutSeconds)}),id:s}}async _getNetworkPromise({timeoutId:e,request:t,logs:a,handler:s}){let r,n;try{n=await s.fetchAndCachePut(t)}catch(e){e instanceof Error&&(r=e)}return e&&clearTimeout(e),(r||!n)&&(n=await s.cacheMatch(t)),n}},et=class extends Z{_networkTimeoutSeconds;constructor(e={}){super(e),this._networkTimeoutSeconds=e.networkTimeoutSeconds||0}async _handle(e,t){let a,s;try{let a=[t.fetch(e)];if(this._networkTimeoutSeconds){let e=h(1e3*this._networkTimeoutSeconds);a.push(e)}if(!(s=await Promise.race(a)))throw Error(`Timed out the network response after ${this._networkTimeoutSeconds} seconds.`)}catch(e){e instanceof Error&&(a=e)}if(!s)throw new l("no-response",{url:e.url,error:a});return s}};let ea=e=>e&&"object"==typeof e?e:{handle:e};var es=class{handler;match;method;catchHandler;constructor(e,t,a="GET"){this.handler=ea(t),this.match=e,this.method=a}setCatchHandler(e){this.catchHandler=ea(e)}},er=class e extends Z{_fallbackToNetwork;static defaultPrecacheCacheabilityPlugin={cacheWillUpdate:async({response:e})=>!e||e.status>=400?null:e};static copyRedirectedCacheableResponsesPlugin={cacheWillUpdate:async({response:e})=>e.redirected?await M(e):e};constructor(t={}){t.cacheName=c(t.cacheName),super(t),this._fallbackToNetwork=!1!==t.fallbackToNetwork,this.plugins.push(e.copyRedirectedCacheableResponsesPlugin)}async _handle(e,t){let a=await t.getPreloadResponse();if(a)return a;let s=await t.cacheMatch(e);return s||(t.event&&"install"===t.event.type?await this._handleInstall(e,t):await this._handleFetch(e,t))}async _handleFetch(e,t){let a,s=t.params||{};if(this._fallbackToNetwork){let r=s.integrity,n=e.integrity,i=!n||n===r;a=await t.fetch(new Request(e,{integrity:"no-cors"!==e.mode?n||r:void 0})),r&&i&&"no-cors"!==e.mode&&(this._useDefaultCacheabilityPluginIfNeeded(),await t.cachePut(e,a.clone()))}else throw new l("missing-precache-entry",{cacheName:this.cacheName,url:e.url});return a}async _handleInstall(e,t){this._useDefaultCacheabilityPluginIfNeeded();let a=await t.fetch(e);if(!await t.cachePut(e,a.clone()))throw new l("bad-precaching-response",{url:e.url,status:a.status});return a}_useDefaultCacheabilityPluginIfNeeded(){let t=null,a=0;for(let[s,r]of this.plugins.entries())r!==e.copyRedirectedCacheableResponsesPlugin&&(r===e.defaultPrecacheCacheabilityPlugin&&(t=s),r.cacheWillUpdate&&a++);0===a?this.plugins.push(e.defaultPrecacheCacheabilityPlugin):a>1&&null!==t&&this.plugins.splice(t,1)}},en=class extends es{_allowlist;_denylist;constructor(e,{allowlist:t=[/./],denylist:a=[]}={}){super(e=>this._match(e),e),this._allowlist=t,this._denylist=a}_match({url:e,request:t}){if(t&&"navigate"!==t.mode)return!1;let a=e.pathname+e.search;for(let e of this._denylist)if(e.test(a))return!1;return!!this._allowlist.some(e=>e.test(a))}},ei=class extends es{constructor(e,t,a){super(({url:t})=>{let a=e.exec(t.href);if(a)return t.origin!==location.origin&&0!==a.index?void 0:a.slice(1)},t,a)}};let ec=e=>{if(!e)throw new l("add-to-cache-list-unexpected-type",{entry:e});if("string"==typeof e){let t=new URL(e,location.href);return{cacheKey:t.href,url:t.href}}let{revision:t,url:a}=e;if(!a)throw new l("add-to-cache-list-unexpected-type",{entry:e});if(!t){let e=new URL(a,location.href);return{cacheKey:e.href,url:e.href}}let s=new URL(a,location.href),r=new URL(a,location.href);return s.searchParams.set("__WB_REVISION__",t),{cacheKey:s.href,url:r.href}};var eo=class{updatedURLs=[];notUpdatedURLs=[];handlerWillStart=async({request:e,state:t})=>{t&&(t.originalRequest=e)};cachedResponseWillBeUsed=async({event:e,state:t,cachedResponse:a})=>{if("install"===e.type&&t?.originalRequest&&t.originalRequest instanceof Request){let e=t.originalRequest.url;a?this.notUpdatedURLs.push(e):this.updatedURLs.push(e)}return a}};let el=async(e,t,a)=>{let s=t.map((e,t)=>({index:t,item:e})),r=async e=>{let t=[];for(;;){let r=s.pop();if(!r)return e(t);let n=await a(r.item);t.push({result:n,index:r.index})}},n=Array.from({length:e},()=>new Promise(r));return(await Promise.all(n)).flat().sort((e,t)=>e.indexe.result)};"u">typeof navigator&&/^((?!chrome|android).)*safari/i.test(navigator.userAgent);let eh="cache-entries",eu=e=>{let t=new URL(e,location.href);return t.hash="",t.href};var ed=class{_cacheName;_db=null;constructor(e){this._cacheName=e}_getId(e){return`${this._cacheName}|${eu(e)}`}_upgradeDb(e){let t=e.createObjectStore(eh,{keyPath:"id"});t.createIndex("cacheName","cacheName",{unique:!1}),t.createIndex("timestamp","timestamp",{unique:!1})}_upgradeDbAndDeleteOldDbs(e){this._upgradeDb(e),this._cacheName&&function(e,{blocked:t}={}){let a=indexedDB.deleteDatabase(e);t&&a.addEventListener("blocked",e=>t(e.oldVersion,e)),R(a).then(()=>void 0)}(this._cacheName)}async setTimestamp(e,t){e=eu(e);let a={id:this._getId(e),cacheName:this._cacheName,url:e,timestamp:t},s=(await this.getDb()).transaction(eh,"readwrite",{durability:"relaxed"});await s.store.put(a),await s.done}async getTimestamp(e){return(await (await this.getDb()).get(eh,this._getId(e)))?.timestamp}async expireEntries(e,t){let a=await (await this.getDb()).transaction(eh,"readwrite").store.index("timestamp").openCursor(null,"prev"),s=[],r=0;for(;a;){let n=a.value;n.cacheName===this._cacheName&&(e&&n.timestamp=t?(a.delete(),s.push(n.url)):r++),a=await a.continue()}return s}async getDb(){return this._db||(this._db=await S("serwist-expiration",1,{upgrade:this._upgradeDbAndDeleteOldDbs.bind(this)})),this._db}},em=class{_isRunning=!1;_rerunRequested=!1;_maxEntries;_maxAgeSeconds;_matchOptions;_cacheName;_timestampModel;constructor(e,t={}){this._maxEntries=t.maxEntries,this._maxAgeSeconds=t.maxAgeSeconds,this._matchOptions=t.matchOptions,this._cacheName=e,this._timestampModel=new ed(e)}async expireEntries(){if(this._isRunning){this._rerunRequested=!0;return}this._isRunning=!0;let e=this._maxAgeSeconds?Date.now()-1e3*this._maxAgeSeconds:0,t=await this._timestampModel.expireEntries(e,this._maxEntries),a=await self.caches.open(this._cacheName);for(let e of t)await a.delete(e,this._matchOptions);this._isRunning=!1,this._rerunRequested&&(this._rerunRequested=!1,this.expireEntries())}async updateTimestamp(e){await this._timestampModel.setTimestamp(e,Date.now())}async isURLExpired(e){if(!this._maxAgeSeconds)return!1;let t=await this._timestampModel.getTimestamp(e),a=Date.now()-1e3*this._maxAgeSeconds;return void 0===t||t{u.add(e)})(()=>this.deleteCacheAndMetadata())}_getCacheExpiration(e){if(e===o())throw new l("expire-custom-caches-only");let t=this._cacheExpirations.get(e);return t||(t=new em(e,this._config),this._cacheExpirations.set(e,t)),t}cachedResponseWillBeUsed({event:e,cacheName:t,request:a,cachedResponse:s}){if(!s)return null;let r=this._isResponseDateFresh(s),n=this._getCacheExpiration(t),i="last-used"===this._config.maxAgeFrom,c=(async()=>{i&&await n.updateTimestamp(a.url),await n.expireEntries()})();try{e.waitUntil(c)}catch{}return r?s:null}_isResponseDateFresh(e){if("last-used"===this._config.maxAgeFrom)return!0;let t=Date.now();if(!this._config.maxAgeSeconds)return!0;let a=this._getDateHeaderTimestamp(e);return null===a||a>=t-1e3*this._config.maxAgeSeconds}_getDateHeaderTimestamp(e){if(!e.headers.has("date"))return null;let t=new Date(e.headers.get("date")).getTime();return Number.isNaN(t)?null:t}async cacheDidUpdate({cacheName:e,request:t}){let a=this._getCacheExpiration(e);await a.updateTimestamp(t.url),await a.expireEntries()}async deleteCacheAndMetadata(){for(let[e,t]of this._cacheExpirations)await self.caches.delete(e),await t.delete();this._cacheExpirations=new Map}};let eg=/^\/(\w+\/)?collect/,ew=({serwist:e,cacheName:t,...a})=>{let s,r,c=t||i(n.googleAnalytics),o=new z("serwist-google-analytics",{maxRetentionTime:2880,onSync:async({queue:e})=>{let t;for(;t=await e.shiftRequest();){let{request:s,timestamp:r}=t,n=new URL(s.url);try{let e="POST"===s.method?new URLSearchParams(await s.clone().text()):n.searchParams,t=r-(Number(e.get("qt"))||0),i=Date.now()-t;if(e.set("qt",String(i)),a.parameterOverrides)for(let t of Object.keys(a.parameterOverrides)){let s=a.parameterOverrides[t];e.set(t,s)}"function"==typeof a.hitFilter&&a.hitFilter.call(null,e),await fetch(new Request(n.origin+n.pathname,{body:e.toString(),method:"POST",mode:"cors",credentials:"omit",headers:{"Content-Type":"text/plain"}}))}catch(a){throw await e.unshiftRequest(t),a}}}});for(let t of[new es(({url:e})=>"www.googletagmanager.com"===e.hostname&&"/gtm.js"===e.pathname,new ee({cacheName:c}),"GET"),new es(({url:e})=>"www.google-analytics.com"===e.hostname&&"/analytics.js"===e.pathname,new ee({cacheName:c}),"GET"),new es(({url:e})=>"www.googletagmanager.com"===e.hostname&&"/gtag/js"===e.pathname,new ee({cacheName:c}),"GET"),new es(s=({url:e})=>"www.google-analytics.com"===e.hostname&&eg.test(e.pathname),r=new et({plugins:[o]}),"GET"),new es(s,r,"POST")])e.registerRoute(t)};var ep=class{_fallbackUrls;_serwist;constructor({fallbackUrls:e,serwist:t}){this._fallbackUrls=e,this._serwist=t}async handlerDidError(e){for(let t of this._fallbackUrls)if("string"==typeof t){let e=await this._serwist.matchPrecache(t);if(void 0!==e)return e}else if(t.matcher(e)){let e=await this._serwist.matchPrecache(t.url);if(void 0!==e)return e}}};let ey=async(e,t)=>{try{if(206===t.status)return t;let a=e.headers.get("range");if(!a)throw new l("no-range-header");let s=(e=>{let t=e.trim().toLowerCase();if(!t.startsWith("bytes="))throw new l("unit-must-be-bytes",{normalizedRangeHeader:t});if(t.includes(","))throw new l("single-range-only",{normalizedRangeHeader:t});let a=/(\d*)-(\d*)/.exec(t);if(!a||!(a[1]||a[2]))throw new l("invalid-range-values",{normalizedRangeHeader:t});return{start:""===a[1]?void 0:Number(a[1]),end:""===a[2]?void 0:Number(a[2])}})(a),r=await t.blob(),n=((e,t,a)=>{let s,r,n=e.size;if(a&&a>n||t&&t<0)throw new l("range-not-satisfiable",{size:n,end:a,start:t});return void 0!==t&&void 0!==a?(s=t,r=a+1):void 0!==t&&void 0===a?(s=t,r=n):void 0!==a&&void 0===t&&(s=n-a,r=n),{start:s,end:r}})(r,s.start,s.end),i=r.slice(n.start,n.end),c=i.size,o=new Response(i,{status:206,statusText:"Partial Content",headers:t.headers});return o.headers.set("Content-Length",String(c)),o.headers.set("Content-Range",`bytes ${n.start}-${n.end-1}/${r.size}`),o}catch(e){return new Response("",{status:416,statusText:"Range Not Satisfiable"})}};var e_=class{cachedResponseWillBeUsed=async({request:e,cachedResponse:t})=>t&&e.headers.has("range")?await ey(e,t):t},ex=class extends Z{async _handle(e,t){let a,s=await t.cacheMatch(e);if(s);else try{s=await t.fetchAndCachePut(e)}catch(e){e instanceof Error&&(a=e)}if(!s)throw new l("no-response",{url:e.url,error:a});return s}},eb=class extends Z{constructor(e={}){super(e),this.plugins.some(e=>"cacheWillUpdate"in e)||this.plugins.unshift(Y)}async _handle(e,t){let a,s=t.fetchAndCachePut(e).catch(()=>{});t.waitUntil(s);let r=await t.cacheMatch(e);if(r);else try{r=await s}catch(e){e instanceof Error&&(a=e)}if(!r)throw new l("no-response",{url:e.url,error:a});return r}},ev=class extends es{constructor(e,t){super(({request:a})=>{let s=e.getUrlsToPrecacheKeys();for(let r of function*(e,{directoryIndex:t="index.html",ignoreURLParametersMatching:a=[/^utm_/,/^fbclid$/],cleanURLs:s=!0,urlManipulation:r}={}){let n=new URL(e,location.href);n.hash="",yield n.href;let i=((e,t=[])=>{for(let a of[...e.searchParams.keys()])t.some(e=>e.test(a))&&e.searchParams.delete(a);return e})(n,a);if(yield i.href,t&&i.pathname.endsWith("/")){let e=new URL(i.href);e.pathname+=t,yield e.href}if(s){let e=new URL(i.href);e.pathname+=".html",yield e.href}if(r)for(let e of r({url:n}))yield e.href}(a.url,t)){let t=s.get(r);if(t)return{cacheKey:t,integrity:e.getIntegrityForPrecacheKey(t)}}},e.precacheStrategy)}},eE=class{_precacheController;constructor({precacheController:e}){this._precacheController=e}cacheKeyWillBeUsed=async({request:e,params:t})=>{let a=t?.cacheKey||this._precacheController.getPrecacheKeyForUrl(e.url);return a?new Request(a,{headers:e.headers}):e}},eR=class{_urlsToCacheKeys=new Map;_urlsToCacheModes=new Map;_cacheKeysToIntegrities=new Map;_concurrentPrecaching;_precacheStrategy;_routes;_defaultHandlerMap;_catchHandler;_requestRules;constructor({precacheEntries:e,precacheOptions:t,skipWaiting:a=!1,importScripts:s,navigationPreload:r=!1,cacheId:i,clientsClaim:o=!1,runtimeCaching:l,offlineAnalyticsConfig:h,disableDevLogs:u=!1,fallbacks:d,requestRules:m}={}){const{precacheStrategyOptions:f,precacheRouteOptions:g,precacheMiscOptions:w}=((e,t={})=>{let{cacheName:a,plugins:s=[],fetchOptions:r,matchOptions:n,fallbackToNetwork:i,directoryIndex:o,ignoreURLParametersMatching:l,cleanURLs:h,urlManipulation:u,cleanupOutdatedCaches:d,concurrency:m=10,navigateFallback:f,navigateFallbackAllowlist:g,navigateFallbackDenylist:w}=t??{};return{precacheStrategyOptions:{cacheName:c(a),plugins:[...s,new eE({precacheController:e})],fetchOptions:r,matchOptions:n,fallbackToNetwork:i},precacheRouteOptions:{directoryIndex:o,ignoreURLParametersMatching:l,cleanURLs:h,urlManipulation:u},precacheMiscOptions:{cleanupOutdatedCaches:d,concurrency:m,navigateFallback:f,navigateFallbackAllowlist:g,navigateFallbackDenylist:w}}})(this,t);if(this._concurrentPrecaching=w.concurrency,this._precacheStrategy=new er(f),this._routes=new Map,this._defaultHandlerMap=new Map,this._requestRules=m,this.handleInstall=this.handleInstall.bind(this),this.handleActivate=this.handleActivate.bind(this),this.handleFetch=this.handleFetch.bind(this),this.handleCache=this.handleCache.bind(this),s&&s.length>0&&self.importScripts(...s),r&&self.registration?.navigationPreload&&self.addEventListener("activate",e=>{e.waitUntil(self.registration.navigationPreload.enable().then(()=>{}))}),void 0!==i&&(e=>{var t=e;for(let e of Object.keys(n))(e=>{let a=t[e];"string"==typeof a&&(n[e]=a)})(e)})({prefix:i}),a?self.skipWaiting():self.addEventListener("message",e=>{e.data&&"SKIP_WAITING"===e.data.type&&self.skipWaiting()}),o&&self.addEventListener("activate",()=>self.clients.claim()),e&&e.length>0&&this.addToPrecacheList(e),w.cleanupOutdatedCaches&&(e=>{self.addEventListener("activate",t=>{t.waitUntil(p(c(e)).then(e=>{}))})})(f.cacheName),this.registerRoute(new ev(this,g)),w.navigateFallback&&this.registerRoute(new en(this.createHandlerBoundToUrl(w.navigateFallback),{allowlist:w.navigateFallbackAllowlist,denylist:w.navigateFallbackDenylist})),void 0!==h&&("boolean"==typeof h?h&&ew({serwist:this}):ew({...h,serwist:this})),void 0!==l){if(void 0!==d){const e=new ep({fallbackUrls:d.entries,serwist:this});l.forEach(t=>{t.handler instanceof Z&&!t.handler.plugins.some(e=>"handlerDidError"in e)&&t.handler.plugins.push(e)})}for(const e of l)this.registerCapture(e.matcher,e.handler,e.method)}u&&(self.__WB_DISABLE_DEV_LOGS=!0)}get precacheStrategy(){return this._precacheStrategy}get routes(){return this._routes}addEventListeners(){self.addEventListener("install",this.handleInstall),self.addEventListener("activate",this.handleActivate),self.addEventListener("fetch",this.handleFetch),self.addEventListener("message",this.handleCache)}addToPrecacheList(e){let t=[];for(let a of e){"string"==typeof a?t.push(a):a&&!a.integrity&&void 0===a.revision&&t.push(a.url);let{cacheKey:e,url:s}=ec(a),r="string"!=typeof a&&a.revision?"reload":"default";if(this._urlsToCacheKeys.has(s)&&this._urlsToCacheKeys.get(s)!==e)throw new l("add-to-cache-list-conflicting-entries",{firstEntry:this._urlsToCacheKeys.get(s),secondEntry:e});if("string"!=typeof a&&a.integrity){if(this._cacheKeysToIntegrities.has(e)&&this._cacheKeysToIntegrities.get(e)!==a.integrity)throw new l("add-to-cache-list-conflicting-integrities",{url:s});this._cacheKeysToIntegrities.set(e,a.integrity)}this._urlsToCacheKeys.set(s,e),this._urlsToCacheModes.set(s,r)}t.length>0&&console.warn(`Serwist is precaching URLs without revision info: ${t.join(", ")} -This is generally NOT safe. Learn more at https://bit.ly/wb-precache`)}handleInstall(e){return this.registerRequestRules(e),y(e,async()=>{let t=new eo;this.precacheStrategy.plugins.push(t),await el(this._concurrentPrecaching,Array.from(this._urlsToCacheKeys.entries()),async([t,a])=>{let s=this._cacheKeysToIntegrities.get(a),r=this._urlsToCacheModes.get(t),n=new Request(t,{integrity:s,cache:r,credentials:"same-origin"});await Promise.all(this.precacheStrategy.handleAll({event:e,request:n,url:new URL(n.url),params:{cacheKey:a}}))});let{updatedURLs:a,notUpdatedURLs:s}=t;return{updatedURLs:a,notUpdatedURLs:s}})}async registerRequestRules(e){if(this._requestRules&&e?.addRoutes)try{await e.addRoutes(this._requestRules),this._requestRules=void 0}catch(e){throw e}}handleActivate(e){return y(e,async()=>{let e=await self.caches.open(this.precacheStrategy.cacheName),t=await e.keys(),a=new Set(this._urlsToCacheKeys.values()),s=[];for(let r of t)a.has(r.url)||(await e.delete(r),s.push(r.url));return{deletedCacheRequests:s}})}handleFetch(e){let{request:t}=e,a=this.handleRequest({request:t,event:e});a&&e.respondWith(a)}handleCache(e){if(e.data&&"CACHE_URLS"===e.data.type){let{payload:t}=e.data,a=Promise.all(t.urlsToCache.map(t=>{let a;return a="string"==typeof t?new Request(t):new Request(...t),this.handleRequest({request:a,event:e})}));e.waitUntil(a),e.ports?.[0]&&a.then(()=>e.ports[0].postMessage(!0))}}setDefaultHandler(e,t="GET"){this._defaultHandlerMap.set(t,ea(e))}setCatchHandler(e){this._catchHandler=ea(e)}registerCapture(e,t,a){let s=((e,t,a)=>{if("string"==typeof e){let s=new URL(e,location.href);return new es(({url:e})=>e.href===s.href,t,a)}if(e instanceof RegExp)return new ei(e,t,a);if("function"==typeof e)return new es(e,t,a);if(e instanceof es)return e;throw new l("unsupported-route-type",{moduleName:"serwist",funcName:"parseRoute",paramName:"capture"})})(e,t,a);return this.registerRoute(s),s}registerRoute(e){this._routes.has(e.method)||this._routes.set(e.method,[]),this._routes.get(e.method).push(e)}unregisterRoute(e){if(!this._routes.has(e.method))throw new l("unregister-route-but-not-found-with-method",{method:e.method});let t=this._routes.get(e.method).indexOf(e);if(t>-1)this._routes.get(e.method).splice(t,1);else throw new l("unregister-route-route-not-registered")}getUrlsToPrecacheKeys(){return this._urlsToCacheKeys}getPrecachedUrls(){return[...this._urlsToCacheKeys.keys()]}getPrecacheKeyForUrl(e){let t=new URL(e,location.href);return this._urlsToCacheKeys.get(t.href)}getIntegrityForPrecacheKey(e){return this._cacheKeysToIntegrities.get(e)}async matchPrecache(e){let t=e instanceof Request?e.url:e,a=this.getPrecacheKeyForUrl(t);if(a)return(await self.caches.open(this.precacheStrategy.cacheName)).match(a)}createHandlerBoundToUrl(e){let t=this.getPrecacheKeyForUrl(e);if(!t)throw new l("non-precached-url",{url:e});return a=>(a.request=new Request(e),a.params={cacheKey:t,...a.params},this.precacheStrategy.handle(a))}handleRequest({request:e,event:t}){let a,s=new URL(e.url,location.href);if(!s.protocol.startsWith("http"))return;let r=s.origin===location.origin,{params:n,route:i}=this.findMatchingRoute({event:t,request:e,sameOrigin:r,url:s}),c=i?.handler,o=e.method;if(!c&&this._defaultHandlerMap.has(o)&&(c=this._defaultHandlerMap.get(o)),!c)return;try{a=c.handle({url:s,request:e,event:t,params:n})}catch(e){a=Promise.reject(e)}let l=i?.catchHandler;return a instanceof Promise&&(this._catchHandler||l)&&(a=a.catch(async a=>{if(l)try{return await l.handle({url:s,request:e,event:t,params:n})}catch(e){e instanceof Error&&(a=e)}if(this._catchHandler)return this._catchHandler.handle({url:s,request:e,event:t});throw a})),a}findMatchingRoute({url:e,sameOrigin:t,request:a,event:s}){for(let r of this._routes.get(a.method)||[]){let n,i=r.match({url:e,sameOrigin:t,request:a,event:s});if(i)return Array.isArray(n=i)&&0===n.length||i.constructor===Object&&0===Object.keys(i).length?n=void 0:"boolean"==typeof i&&(n=void 0),{route:r,params:n}}return{}}};let eq=[{matcher:/^https:\/\/fonts\.(?:gstatic)\.com\/.*/i,handler:new ex({cacheName:"google-fonts-webfonts",plugins:[new ef({maxEntries:4,maxAgeSeconds:31536e3,maxAgeFrom:"last-used"})]})},{matcher:/^https:\/\/fonts\.(?:googleapis)\.com\/.*/i,handler:new eb({cacheName:"google-fonts-stylesheets",plugins:[new ef({maxEntries:4,maxAgeSeconds:604800,maxAgeFrom:"last-used"})]})},{matcher:/\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,handler:new eb({cacheName:"static-font-assets",plugins:[new ef({maxEntries:4,maxAgeSeconds:604800,maxAgeFrom:"last-used"})]})},{matcher:/\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,handler:new eb({cacheName:"static-image-assets",plugins:[new ef({maxEntries:64,maxAgeSeconds:2592e3,maxAgeFrom:"last-used"})]})},{matcher:/\/_next\/static.+\.js$/i,handler:new ex({cacheName:"next-static-js-assets",plugins:[new ef({maxEntries:64,maxAgeSeconds:86400,maxAgeFrom:"last-used"})]})},{matcher:/\/_next\/image\?url=.+$/i,handler:new eb({cacheName:"next-image",plugins:[new ef({maxEntries:64,maxAgeSeconds:86400,maxAgeFrom:"last-used"})]})},{matcher:/\.(?:mp3|wav|ogg)$/i,handler:new ex({cacheName:"static-audio-assets",plugins:[new ef({maxEntries:32,maxAgeSeconds:86400,maxAgeFrom:"last-used"}),new e_]})},{matcher:/\.(?:mp4|webm)$/i,handler:new ex({cacheName:"static-video-assets",plugins:[new ef({maxEntries:32,maxAgeSeconds:86400,maxAgeFrom:"last-used"}),new e_]})},{matcher:/\.(?:js)$/i,handler:new eb({cacheName:"static-js-assets",plugins:[new ef({maxEntries:48,maxAgeSeconds:86400,maxAgeFrom:"last-used"})]})},{matcher:/\.(?:css|less)$/i,handler:new eb({cacheName:"static-style-assets",plugins:[new ef({maxEntries:32,maxAgeSeconds:86400,maxAgeFrom:"last-used"})]})},{matcher:/\/_next\/data\/.+\/.+\.json$/i,handler:new ee({cacheName:"next-data",plugins:[new ef({maxEntries:32,maxAgeSeconds:86400,maxAgeFrom:"last-used"})]})},{matcher:/\.(?:json|xml|csv)$/i,handler:new ee({cacheName:"static-data-assets",plugins:[new ef({maxEntries:32,maxAgeSeconds:86400,maxAgeFrom:"last-used"})]})},{matcher:/\/api\/auth\/.*/,handler:new et({networkTimeoutSeconds:10})},{matcher:({sameOrigin:e,url:{pathname:t}})=>e&&t.startsWith("/api/"),method:"GET",handler:new ee({cacheName:"apis",plugins:[new ef({maxEntries:16,maxAgeSeconds:86400,maxAgeFrom:"last-used"})],networkTimeoutSeconds:10})},{matcher:({request:e,url:{pathname:t},sameOrigin:a})=>"1"===e.headers.get("RSC")&&"1"===e.headers.get("Next-Router-Prefetch")&&a&&!t.startsWith("/api/"),handler:new ee({cacheName:"pages-rsc-prefetch",plugins:[new ef({maxEntries:32,maxAgeSeconds:86400})]})},{matcher:({request:e,url:{pathname:t},sameOrigin:a})=>"1"===e.headers.get("RSC")&&a&&!t.startsWith("/api/"),handler:new ee({cacheName:"pages-rsc",plugins:[new ef({maxEntries:32,maxAgeSeconds:86400})]})},{matcher:({request:e,url:{pathname:t},sameOrigin:a})=>e.headers.get("Content-Type")?.includes("text/html")&&a&&!t.startsWith("/api/"),handler:new ee({cacheName:"pages",plugins:[new ef({maxEntries:32,maxAgeSeconds:86400})]})},{matcher:({url:{pathname:e},sameOrigin:t})=>t&&!e.startsWith("/api/"),handler:new ee({cacheName:"others",plugins:[new ef({maxEntries:32,maxAgeSeconds:86400})]})},{matcher:({sameOrigin:e})=>!e,handler:new ee({cacheName:"cross-origin",plugins:[new ef({maxEntries:32,maxAgeSeconds:3600})],networkTimeoutSeconds:10})},{matcher:/.*/i,method:"GET",handler:new et}];new eR({precacheEntries:[{'revision':null,'url':'/_next/static/chunks/1958-a1cd681eb2c1a83e.js'},{'revision':null,'url':'/_next/static/chunks/4bd1b696-c2f6e0877b6c10aa.js'},{'revision':null,'url':'/_next/static/chunks/5838-7ac6ecc648315323.js'},{'revision':null,'url':'/_next/static/chunks/6810-8b074082a0b51859.js'},{'revision':null,'url':'/_next/static/chunks/8500-3953cc33eeceb44e.js'},{'revision':null,'url':'/_next/static/chunks/9048-05afa5c60c3f117a.js'},{'revision':null,'url':'/_next/static/chunks/9da6db1e-54d62c4d2564a0ac.js'},{'revision':null,'url':'/_next/static/chunks/app/_global-error/page-17ca8364c0815cb4.js'},{'revision':null,'url':'/_next/static/chunks/app/_not-found/page-17ca8364c0815cb4.js'},{'revision':null,'url':'/_next/static/chunks/app/api/checkout/route-17ca8364c0815cb4.js'},{'revision':null,'url':'/_next/static/chunks/app/api/games/%5Bid%5D/props/route-17ca8364c0815cb4.js'},{'revision':null,'url':'/_next/static/chunks/app/api/games/%5Bid%5D/route-17ca8364c0815cb4.js'},{'revision':null,'url':'/_next/static/chunks/app/api/games/tonight/route-17ca8364c0815cb4.js'},{'revision':null,'url':'/_next/static/chunks/app/api/intelligence/feed/route-17ca8364c0815cb4.js'},{'revision':null,'url':'/_next/static/chunks/app/api/ledger/accuracy/route-17ca8364c0815cb4.js'},{'revision':null,'url':'/_next/static/chunks/app/api/ledger/route-17ca8364c0815cb4.js'},{'revision':null,'url':'/_next/static/chunks/app/api/odds/soccer/%5Bleague%5D/route-17ca8364c0815cb4.js'},{'revision':null,'url':'/_next/static/chunks/app/api/parlay/add-leg/route-17ca8364c0815cb4.js'},{'revision':null,'url':'/_next/static/chunks/app/api/parlay/grade/route-17ca8364c0815cb4.js'},{'revision':null,'url':'/_next/static/chunks/app/api/players/search/route-17ca8364c0815cb4.js'},{'revision':null,'url':'/_next/static/chunks/app/api/props/live/route-17ca8364c0815cb4.js'},{'revision':null,'url':'/_next/static/chunks/app/api/props/most-parlayed/route-17ca8364c0815cb4.js'},{'revision':null,'url':'/_next/static/chunks/app/api/props/top-graded/route-17ca8364c0815cb4.js'},{'revision':null,'url':'/_next/static/chunks/app/api/scan/route-17ca8364c0815cb4.js'},{'revision':null,'url':'/_next/static/chunks/app/api/stats/parlays-graded/route-17ca8364c0815cb4.js'},{'revision':null,'url':'/_next/static/chunks/app/api/stats/public/route-17ca8364c0815cb4.js'},{'revision':null,'url':'/_next/static/chunks/app/api/user/profile/route-17ca8364c0815cb4.js'},{'revision':null,'url':'/_next/static/chunks/app/api/user/recent-scans/route-17ca8364c0815cb4.js'},{'revision':null,'url':'/_next/static/chunks/app/api/user/scans/route-17ca8364c0815cb4.js'},{'revision':null,'url':'/_next/static/chunks/app/api/waitlist/route-17ca8364c0815cb4.js'},{'revision':null,'url':'/_next/static/chunks/app/api/webhook/nexapay/route-17ca8364c0815cb4.js'},{'revision':null,'url':'/_next/static/chunks/app/auth/callback/page-f6253334ccab6a26.js'},{'revision':null,'url':'/_next/static/chunks/app/blog/%5Bslug%5D/page-17ca8364c0815cb4.js'},{'revision':null,'url':'/_next/static/chunks/app/blog/page-17ca8364c0815cb4.js'},{'revision':null,'url':'/_next/static/chunks/app/dashboard/page-107967f64e4a489c.js'},{'revision':null,'url':'/_next/static/chunks/app/forgot-password/page-012a02e7c1c1bf4c.js'},{'revision':null,'url':'/_next/static/chunks/app/game/%5Bid%5D/page-2c1eb07021c4e596.js'},{'revision':null,'url':'/_next/static/chunks/app/intelligence/page-67a1ff5cb5db4b20.js'},{'revision':null,'url':'/_next/static/chunks/app/layout-0664074f4773364b.js'},{'revision':null,'url':'/_next/static/chunks/app/ledger/page-b2f371ae0e2dc445.js'},{'revision':null,'url':'/_next/static/chunks/app/login/page-e0e44e4f9dfb8d77.js'},{'revision':null,'url':'/_next/static/chunks/app/marketplace/page-ce0df41afd789a48.js'},{'revision':null,'url':'/_next/static/chunks/app/not-found-3d8359a06d3506dc.js'},{'revision':null,'url':'/_next/static/chunks/app/page-f57ab420b87965db.js'},{'revision':null,'url':'/_next/static/chunks/app/pricing/page-26ab42eb33b18416.js'},{'revision':null,'url':'/_next/static/chunks/app/privacy/page-17ca8364c0815cb4.js'},{'revision':null,'url':'/_next/static/chunks/app/profile/page-589cb3a364727c3b.js'},{'revision':null,'url':'/_next/static/chunks/app/responsible-gambling/page-17ca8364c0815cb4.js'},{'revision':null,'url':'/_next/static/chunks/app/scan/page-ee1e7545074ffd2a.js'},{'revision':null,'url':'/_next/static/chunks/app/settings/security/page-6728e10517c834af.js'},{'revision':null,'url':'/_next/static/chunks/app/signup/page-06c72711414c663d.js'},{'revision':null,'url':'/_next/static/chunks/app/soccer/page-17f39664cea5f0ca.js'},{'revision':null,'url':'/_next/static/chunks/app/terms/page-17ca8364c0815cb4.js'},{'revision':null,'url':'/_next/static/chunks/app/tracker/page-c75c1f66c42eac58.js'},{'revision':null,'url':'/_next/static/chunks/app/upgrade/cancel/page-3d8359a06d3506dc.js'},{'revision':null,'url':'/_next/static/chunks/app/upgrade/desk/page-416ebb86e698df18.js'},{'revision':null,'url':'/_next/static/chunks/app/upgrade/success/page-3057bec4679f1ad3.js'},{'revision':null,'url':'/_next/static/chunks/app/verify/page-b5ca90220207b0ae.js'},{'revision':null,'url':'/_next/static/chunks/app/welcome/page-ff77b94f609b0d52.js'},{'revision':null,'url':'/_next/static/chunks/framework-29591c88a1db84ab.js'},{'revision':null,'url':'/_next/static/chunks/main-ad5ed0494576592d.js'},{'revision':null,'url':'/_next/static/chunks/main-app-f54c179578b140aa.js'},{'revision':null,'url':'/_next/static/chunks/next/dist/client/components/builtin/app-error-17ca8364c0815cb4.js'},{'revision':null,'url':'/_next/static/chunks/next/dist/client/components/builtin/forbidden-17ca8364c0815cb4.js'},{'revision':null,'url':'/_next/static/chunks/next/dist/client/components/builtin/global-error-d5beecc31181c673.js'},{'revision':null,'url':'/_next/static/chunks/next/dist/client/components/builtin/unauthorized-17ca8364c0815cb4.js'},{'revision':'846118c33b2c0e922d7b3a7676f81f6f','url':'/_next/static/chunks/polyfills-42372ed130431b0a.js'},{'revision':null,'url':'/_next/static/chunks/webpack-9660626b6ab2c75c.js'},{'revision':null,'url':'/_next/static/css/424869ae6d53eea0.css'},{'revision':'351b8d8f1191d818da0829e300463beb','url':'/_next/static/wwWA3m8FwiwmMiltFfuqN/_buildManifest.js'},{'revision':'b6652df95db52feb4daf4eca35380933','url':'/_next/static/wwWA3m8FwiwmMiltFfuqN/_ssgManifest.js'},{'revision':'d7bc0f63f9618b8b700d028d7d242de8','url':'/apple-touch-icon.png'},{'revision':'5616f81ec5f8a9f726a17fb7ac66a54a','url':'/favicon-16.png'},{'revision':'1fc285cfb0116a0523eb34c779a7d282','url':'/favicon-32.png'},{'revision':'583b034f36839a8af92841771eef38b6','url':'/favicon.ico'},{'revision':'b15795128b3691e5703d0ce14ce10827','url':'/favicon.png'},{'revision':'44e9d71551be13574ac2e10063d03aa9','url':'/favicon.svg'},{'revision':'252b8f8c4d0f64c7cd120f182a6c74ab','url':'/icon-192.png'},{'revision':'3e3b53bbd210bd772b36b70165657e7c','url':'/icon-512.png'},{'revision':'252b8f8c4d0f64c7cd120f182a6c74ab','url':'/icons/icon-192.png'},{'revision':'3e3b53bbd210bd772b36b70165657e7c','url':'/icons/icon-512.png'},{'revision':'3e3b53bbd210bd772b36b70165657e7c','url':'/icons/icon-maskable-512.png'},{'revision':'27007d181514e7fe4213035736a8300e','url':'/manifest.json'},{'revision':'9f61cf51298661d5c57a782534216240','url':'/og-image.png'},{'revision':'7c759c20b35840eacf9344692cb51061','url':'/og-image.svg'},{'revision':'9327802333275f11e68c3f19f20c160d','url':'/widget.js'}],skipWaiting:!0,clientsClaim:!0,navigationPreload:!0,runtimeCaching:eq}).addEventListeners(),self.addEventListener("push",e=>{let t;if(!e.data)return;try{t=e.data.json()}catch{t={title:"VYNDR",body:e.data.text()}}let{title:a="VYNDR",body:s="",icon:r="/icons/icon-192.png",url:n="/"}=t;e.waitUntil(self.registration.showNotification(a,{body:s,icon:r,badge:"/icons/icon-192.png",data:{url:n}}))}),self.addEventListener("notificationclick",e=>{e.notification.close();let t=e.notification.data?.url??"/";e.waitUntil(self.clients.matchAll({type:"window",includeUncontrolled:!0}).then(e=>{let a=e.find(e=>e.url.endsWith(t));return a?a.focus():self.clients.openWindow(t)}))})})(); \ No newline at end of file +This is generally NOT safe. Learn more at https://bit.ly/wb-precache`)}handleInstall(e){return this.registerRequestRules(e),y(e,async()=>{let t=new eo;this.precacheStrategy.plugins.push(t),await el(this._concurrentPrecaching,Array.from(this._urlsToCacheKeys.entries()),async([t,a])=>{let s=this._cacheKeysToIntegrities.get(a),r=this._urlsToCacheModes.get(t),n=new Request(t,{integrity:s,cache:r,credentials:"same-origin"});await Promise.all(this.precacheStrategy.handleAll({event:e,request:n,url:new URL(n.url),params:{cacheKey:a}}))});let{updatedURLs:a,notUpdatedURLs:s}=t;return{updatedURLs:a,notUpdatedURLs:s}})}async registerRequestRules(e){if(this._requestRules&&e?.addRoutes)try{await e.addRoutes(this._requestRules),this._requestRules=void 0}catch(e){throw e}}handleActivate(e){return y(e,async()=>{let e=await self.caches.open(this.precacheStrategy.cacheName),t=await e.keys(),a=new Set(this._urlsToCacheKeys.values()),s=[];for(let r of t)a.has(r.url)||(await e.delete(r),s.push(r.url));return{deletedCacheRequests:s}})}handleFetch(e){let{request:t}=e,a=this.handleRequest({request:t,event:e});a&&e.respondWith(a)}handleCache(e){if(e.data&&"CACHE_URLS"===e.data.type){let{payload:t}=e.data,a=Promise.all(t.urlsToCache.map(t=>{let a;return a="string"==typeof t?new Request(t):new Request(...t),this.handleRequest({request:a,event:e})}));e.waitUntil(a),e.ports?.[0]&&a.then(()=>e.ports[0].postMessage(!0))}}setDefaultHandler(e,t="GET"){this._defaultHandlerMap.set(t,ea(e))}setCatchHandler(e){this._catchHandler=ea(e)}registerCapture(e,t,a){let s=((e,t,a)=>{if("string"==typeof e){let s=new URL(e,location.href);return new es(({url:e})=>e.href===s.href,t,a)}if(e instanceof RegExp)return new ei(e,t,a);if("function"==typeof e)return new es(e,t,a);if(e instanceof es)return e;throw new l("unsupported-route-type",{moduleName:"serwist",funcName:"parseRoute",paramName:"capture"})})(e,t,a);return this.registerRoute(s),s}registerRoute(e){this._routes.has(e.method)||this._routes.set(e.method,[]),this._routes.get(e.method).push(e)}unregisterRoute(e){if(!this._routes.has(e.method))throw new l("unregister-route-but-not-found-with-method",{method:e.method});let t=this._routes.get(e.method).indexOf(e);if(t>-1)this._routes.get(e.method).splice(t,1);else throw new l("unregister-route-route-not-registered")}getUrlsToPrecacheKeys(){return this._urlsToCacheKeys}getPrecachedUrls(){return[...this._urlsToCacheKeys.keys()]}getPrecacheKeyForUrl(e){let t=new URL(e,location.href);return this._urlsToCacheKeys.get(t.href)}getIntegrityForPrecacheKey(e){return this._cacheKeysToIntegrities.get(e)}async matchPrecache(e){let t=e instanceof Request?e.url:e,a=this.getPrecacheKeyForUrl(t);if(a)return(await self.caches.open(this.precacheStrategy.cacheName)).match(a)}createHandlerBoundToUrl(e){let t=this.getPrecacheKeyForUrl(e);if(!t)throw new l("non-precached-url",{url:e});return a=>(a.request=new Request(e),a.params={cacheKey:t,...a.params},this.precacheStrategy.handle(a))}handleRequest({request:e,event:t}){let a,s=new URL(e.url,location.href);if(!s.protocol.startsWith("http"))return;let r=s.origin===location.origin,{params:n,route:i}=this.findMatchingRoute({event:t,request:e,sameOrigin:r,url:s}),c=i?.handler,o=e.method;if(!c&&this._defaultHandlerMap.has(o)&&(c=this._defaultHandlerMap.get(o)),!c)return;try{a=c.handle({url:s,request:e,event:t,params:n})}catch(e){a=Promise.reject(e)}let l=i?.catchHandler;return a instanceof Promise&&(this._catchHandler||l)&&(a=a.catch(async a=>{if(l)try{return await l.handle({url:s,request:e,event:t,params:n})}catch(e){e instanceof Error&&(a=e)}if(this._catchHandler)return this._catchHandler.handle({url:s,request:e,event:t});throw a})),a}findMatchingRoute({url:e,sameOrigin:t,request:a,event:s}){for(let r of this._routes.get(a.method)||[]){let n,i=r.match({url:e,sameOrigin:t,request:a,event:s});if(i)return Array.isArray(n=i)&&0===n.length||i.constructor===Object&&0===Object.keys(i).length?n=void 0:"boolean"==typeof i&&(n=void 0),{route:r,params:n}}return{}}};let eq=[{matcher:/^https:\/\/fonts\.(?:gstatic)\.com\/.*/i,handler:new ex({cacheName:"google-fonts-webfonts",plugins:[new ef({maxEntries:4,maxAgeSeconds:31536e3,maxAgeFrom:"last-used"})]})},{matcher:/^https:\/\/fonts\.(?:googleapis)\.com\/.*/i,handler:new eb({cacheName:"google-fonts-stylesheets",plugins:[new ef({maxEntries:4,maxAgeSeconds:604800,maxAgeFrom:"last-used"})]})},{matcher:/\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,handler:new eb({cacheName:"static-font-assets",plugins:[new ef({maxEntries:4,maxAgeSeconds:604800,maxAgeFrom:"last-used"})]})},{matcher:/\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,handler:new eb({cacheName:"static-image-assets",plugins:[new ef({maxEntries:64,maxAgeSeconds:2592e3,maxAgeFrom:"last-used"})]})},{matcher:/\/_next\/static.+\.js$/i,handler:new ex({cacheName:"next-static-js-assets",plugins:[new ef({maxEntries:64,maxAgeSeconds:86400,maxAgeFrom:"last-used"})]})},{matcher:/\/_next\/image\?url=.+$/i,handler:new eb({cacheName:"next-image",plugins:[new ef({maxEntries:64,maxAgeSeconds:86400,maxAgeFrom:"last-used"})]})},{matcher:/\.(?:mp3|wav|ogg)$/i,handler:new ex({cacheName:"static-audio-assets",plugins:[new ef({maxEntries:32,maxAgeSeconds:86400,maxAgeFrom:"last-used"}),new e_]})},{matcher:/\.(?:mp4|webm)$/i,handler:new ex({cacheName:"static-video-assets",plugins:[new ef({maxEntries:32,maxAgeSeconds:86400,maxAgeFrom:"last-used"}),new e_]})},{matcher:/\.(?:js)$/i,handler:new eb({cacheName:"static-js-assets",plugins:[new ef({maxEntries:48,maxAgeSeconds:86400,maxAgeFrom:"last-used"})]})},{matcher:/\.(?:css|less)$/i,handler:new eb({cacheName:"static-style-assets",plugins:[new ef({maxEntries:32,maxAgeSeconds:86400,maxAgeFrom:"last-used"})]})},{matcher:/\/_next\/data\/.+\/.+\.json$/i,handler:new ee({cacheName:"next-data",plugins:[new ef({maxEntries:32,maxAgeSeconds:86400,maxAgeFrom:"last-used"})]})},{matcher:/\.(?:json|xml|csv)$/i,handler:new ee({cacheName:"static-data-assets",plugins:[new ef({maxEntries:32,maxAgeSeconds:86400,maxAgeFrom:"last-used"})]})},{matcher:/\/api\/auth\/.*/,handler:new et({networkTimeoutSeconds:10})},{matcher:({sameOrigin:e,url:{pathname:t}})=>e&&t.startsWith("/api/"),method:"GET",handler:new ee({cacheName:"apis",plugins:[new ef({maxEntries:16,maxAgeSeconds:86400,maxAgeFrom:"last-used"})],networkTimeoutSeconds:10})},{matcher:({request:e,url:{pathname:t},sameOrigin:a})=>"1"===e.headers.get("RSC")&&"1"===e.headers.get("Next-Router-Prefetch")&&a&&!t.startsWith("/api/"),handler:new ee({cacheName:"pages-rsc-prefetch",plugins:[new ef({maxEntries:32,maxAgeSeconds:86400})]})},{matcher:({request:e,url:{pathname:t},sameOrigin:a})=>"1"===e.headers.get("RSC")&&a&&!t.startsWith("/api/"),handler:new ee({cacheName:"pages-rsc",plugins:[new ef({maxEntries:32,maxAgeSeconds:86400})]})},{matcher:({request:e,url:{pathname:t},sameOrigin:a})=>e.headers.get("Content-Type")?.includes("text/html")&&a&&!t.startsWith("/api/"),handler:new ee({cacheName:"pages",plugins:[new ef({maxEntries:32,maxAgeSeconds:86400})]})},{matcher:({url:{pathname:e},sameOrigin:t})=>t&&!e.startsWith("/api/"),handler:new ee({cacheName:"others",plugins:[new ef({maxEntries:32,maxAgeSeconds:86400})]})},{matcher:({sameOrigin:e})=>!e,handler:new ee({cacheName:"cross-origin",plugins:[new ef({maxEntries:32,maxAgeSeconds:3600})],networkTimeoutSeconds:10})},{matcher:/.*/i,method:"GET",handler:new et}];new eR({precacheEntries:[{'revision':'a2024411cafeeda69a35577fe57fc766','url':'/_next/static/06fRZjzKuw-1VtfsAVv3x/_buildManifest.js'},{'revision':'b6652df95db52feb4daf4eca35380933','url':'/_next/static/06fRZjzKuw-1VtfsAVv3x/_ssgManifest.js'},{'revision':null,'url':'/_next/static/chunks/1958-a1cd681eb2c1a83e.js'},{'revision':null,'url':'/_next/static/chunks/4bd1b696-c2f6e0877b6c10aa.js'},{'revision':null,'url':'/_next/static/chunks/52774a7f.8203600637b1464d.js'},{'revision':null,'url':'/_next/static/chunks/5838-7ac6ecc648315323.js'},{'revision':null,'url':'/_next/static/chunks/6810-8b074082a0b51859.js'},{'revision':null,'url':'/_next/static/chunks/7602.ffc6bf0443dc19f7.js'},{'revision':null,'url':'/_next/static/chunks/8500-3953cc33eeceb44e.js'},{'revision':null,'url':'/_next/static/chunks/9048-05afa5c60c3f117a.js'},{'revision':null,'url':'/_next/static/chunks/9da6db1e-54d62c4d2564a0ac.js'},{'revision':null,'url':'/_next/static/chunks/app/_global-error/page-539dc17e8248d788.js'},{'revision':null,'url':'/_next/static/chunks/app/_not-found/page-539dc17e8248d788.js'},{'revision':null,'url':'/_next/static/chunks/app/api/checkout/route-539dc17e8248d788.js'},{'revision':null,'url':'/_next/static/chunks/app/api/games/%5Bid%5D/props/route-539dc17e8248d788.js'},{'revision':null,'url':'/_next/static/chunks/app/api/games/%5Bid%5D/route-539dc17e8248d788.js'},{'revision':null,'url':'/_next/static/chunks/app/api/games/tonight/route-539dc17e8248d788.js'},{'revision':null,'url':'/_next/static/chunks/app/api/intelligence/feed/route-539dc17e8248d788.js'},{'revision':null,'url':'/_next/static/chunks/app/api/ledger/accuracy/route-539dc17e8248d788.js'},{'revision':null,'url':'/_next/static/chunks/app/api/ledger/route-539dc17e8248d788.js'},{'revision':null,'url':'/_next/static/chunks/app/api/odds/soccer/%5Bleague%5D/route-539dc17e8248d788.js'},{'revision':null,'url':'/_next/static/chunks/app/api/parlay/add-leg/route-539dc17e8248d788.js'},{'revision':null,'url':'/_next/static/chunks/app/api/parlay/grade/route-539dc17e8248d788.js'},{'revision':null,'url':'/_next/static/chunks/app/api/players/search/route-539dc17e8248d788.js'},{'revision':null,'url':'/_next/static/chunks/app/api/props/live/route-539dc17e8248d788.js'},{'revision':null,'url':'/_next/static/chunks/app/api/props/most-parlayed/route-539dc17e8248d788.js'},{'revision':null,'url':'/_next/static/chunks/app/api/props/top-graded/route-539dc17e8248d788.js'},{'revision':null,'url':'/_next/static/chunks/app/api/scan/route-539dc17e8248d788.js'},{'revision':null,'url':'/_next/static/chunks/app/api/stats/parlays-graded/route-539dc17e8248d788.js'},{'revision':null,'url':'/_next/static/chunks/app/api/stats/public/route-539dc17e8248d788.js'},{'revision':null,'url':'/_next/static/chunks/app/api/user/profile/route-539dc17e8248d788.js'},{'revision':null,'url':'/_next/static/chunks/app/api/user/recent-scans/route-539dc17e8248d788.js'},{'revision':null,'url':'/_next/static/chunks/app/api/user/scans/route-539dc17e8248d788.js'},{'revision':null,'url':'/_next/static/chunks/app/api/waitlist/route-539dc17e8248d788.js'},{'revision':null,'url':'/_next/static/chunks/app/api/webhook/nexapay/route-539dc17e8248d788.js'},{'revision':null,'url':'/_next/static/chunks/app/api/welcome-email/route-539dc17e8248d788.js'},{'revision':null,'url':'/_next/static/chunks/app/auth/callback/page-f6253334ccab6a26.js'},{'revision':null,'url':'/_next/static/chunks/app/blog/%5Bslug%5D/page-539dc17e8248d788.js'},{'revision':null,'url':'/_next/static/chunks/app/blog/page-539dc17e8248d788.js'},{'revision':null,'url':'/_next/static/chunks/app/dashboard/page-107967f64e4a489c.js'},{'revision':null,'url':'/_next/static/chunks/app/forgot-password/page-012a02e7c1c1bf4c.js'},{'revision':null,'url':'/_next/static/chunks/app/game/%5Bid%5D/page-2c1eb07021c4e596.js'},{'revision':null,'url':'/_next/static/chunks/app/intelligence/page-67a1ff5cb5db4b20.js'},{'revision':null,'url':'/_next/static/chunks/app/layout-fb5d019fda290aaf.js'},{'revision':null,'url':'/_next/static/chunks/app/ledger/page-b2f371ae0e2dc445.js'},{'revision':null,'url':'/_next/static/chunks/app/login/page-e0e44e4f9dfb8d77.js'},{'revision':null,'url':'/_next/static/chunks/app/marketplace/page-ce0df41afd789a48.js'},{'revision':null,'url':'/_next/static/chunks/app/not-found-3d8359a06d3506dc.js'},{'revision':null,'url':'/_next/static/chunks/app/page-f57ab420b87965db.js'},{'revision':null,'url':'/_next/static/chunks/app/pricing/page-26ab42eb33b18416.js'},{'revision':null,'url':'/_next/static/chunks/app/privacy/page-539dc17e8248d788.js'},{'revision':null,'url':'/_next/static/chunks/app/profile/page-589cb3a364727c3b.js'},{'revision':null,'url':'/_next/static/chunks/app/responsible-gambling/page-539dc17e8248d788.js'},{'revision':null,'url':'/_next/static/chunks/app/scan/page-ee1e7545074ffd2a.js'},{'revision':null,'url':'/_next/static/chunks/app/settings/security/page-6728e10517c834af.js'},{'revision':null,'url':'/_next/static/chunks/app/signup/page-06c72711414c663d.js'},{'revision':null,'url':'/_next/static/chunks/app/soccer/page-17f39664cea5f0ca.js'},{'revision':null,'url':'/_next/static/chunks/app/terms/page-539dc17e8248d788.js'},{'revision':null,'url':'/_next/static/chunks/app/tracker/page-c75c1f66c42eac58.js'},{'revision':null,'url':'/_next/static/chunks/app/upgrade/cancel/page-3d8359a06d3506dc.js'},{'revision':null,'url':'/_next/static/chunks/app/upgrade/desk/page-416ebb86e698df18.js'},{'revision':null,'url':'/_next/static/chunks/app/upgrade/success/page-3057bec4679f1ad3.js'},{'revision':null,'url':'/_next/static/chunks/app/verify/page-b5ca90220207b0ae.js'},{'revision':null,'url':'/_next/static/chunks/app/welcome/page-8c6156d4a8e8b501.js'},{'revision':null,'url':'/_next/static/chunks/framework-29591c88a1db84ab.js'},{'revision':null,'url':'/_next/static/chunks/main-ad5ed0494576592d.js'},{'revision':null,'url':'/_next/static/chunks/main-app-f54c179578b140aa.js'},{'revision':null,'url':'/_next/static/chunks/next/dist/client/components/builtin/app-error-539dc17e8248d788.js'},{'revision':null,'url':'/_next/static/chunks/next/dist/client/components/builtin/forbidden-539dc17e8248d788.js'},{'revision':null,'url':'/_next/static/chunks/next/dist/client/components/builtin/global-error-d5beecc31181c673.js'},{'revision':null,'url':'/_next/static/chunks/next/dist/client/components/builtin/unauthorized-539dc17e8248d788.js'},{'revision':'846118c33b2c0e922d7b3a7676f81f6f','url':'/_next/static/chunks/polyfills-42372ed130431b0a.js'},{'revision':null,'url':'/_next/static/chunks/webpack-d83542e0eaf1ad4f.js'},{'revision':null,'url':'/_next/static/css/424869ae6d53eea0.css'},{'revision':'d7bc0f63f9618b8b700d028d7d242de8','url':'/apple-touch-icon.png'},{'revision':'5616f81ec5f8a9f726a17fb7ac66a54a','url':'/favicon-16.png'},{'revision':'1fc285cfb0116a0523eb34c779a7d282','url':'/favicon-32.png'},{'revision':'583b034f36839a8af92841771eef38b6','url':'/favicon.ico'},{'revision':'b15795128b3691e5703d0ce14ce10827','url':'/favicon.png'},{'revision':'44e9d71551be13574ac2e10063d03aa9','url':'/favicon.svg'},{'revision':'252b8f8c4d0f64c7cd120f182a6c74ab','url':'/icon-192.png'},{'revision':'3e3b53bbd210bd772b36b70165657e7c','url':'/icon-512.png'},{'revision':'252b8f8c4d0f64c7cd120f182a6c74ab','url':'/icons/icon-192.png'},{'revision':'3e3b53bbd210bd772b36b70165657e7c','url':'/icons/icon-512.png'},{'revision':'3e3b53bbd210bd772b36b70165657e7c','url':'/icons/icon-maskable-512.png'},{'revision':'27007d181514e7fe4213035736a8300e','url':'/manifest.json'},{'revision':'9f61cf51298661d5c57a782534216240','url':'/og-image.png'},{'revision':'7c759c20b35840eacf9344692cb51061','url':'/og-image.svg'},{'revision':'9327802333275f11e68c3f19f20c160d','url':'/widget.js'}],skipWaiting:!0,clientsClaim:!0,navigationPreload:!0,runtimeCaching:eq}).addEventListeners(),self.addEventListener("push",e=>{let t;if(!e.data)return;try{t=e.data.json()}catch{t={title:"VYNDR",body:e.data.text()}}let{title:a="VYNDR",body:s="",icon:r="/icons/icon-192.png",url:n="/"}=t;e.waitUntil(self.registration.showNotification(a,{body:s,icon:r,badge:"/icons/icon-192.png",data:{url:n}}))}),self.addEventListener("notificationclick",e=>{e.notification.close();let t=e.notification.data?.url??"/";e.waitUntil(self.clients.matchAll({type:"window",includeUncontrolled:!0}).then(e=>{let a=e.find(e=>e.url.endsWith(t));return a?a.focus():self.clients.openWindow(t)}))})})(); \ No newline at end of file diff --git a/web/src/app/api/welcome-email/route.ts b/web/src/app/api/welcome-email/route.ts new file mode 100644 index 0000000..689db7a --- /dev/null +++ b/web/src/app/api/welcome-email/route.ts @@ -0,0 +1,80 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getUserFromRequest, jsonError } from '@/lib/auth-helpers'; +import { getServiceRoleSupabase } from '@/lib/supabase'; +import { sendWelcomeEmail } from '@/services/email'; + +export const dynamic = 'force-dynamic'; + +/** + * Welcome email trigger (Session 10). + * + * POST /api/welcome-email + * Bearer auth required. + * + * Idempotent. The send-once flag lives on Supabase auth user_metadata + * (`welcome_email_sent`) — no migration needed; user_metadata is a + * JSONB field Supabase exposes by default. We read it via the + * service-role admin API so the user can't spoof it from the browser. + * + * Response codes: + * 200 { sent: true, id } — email sent successfully + * 200 { sent: false, reason } — skipped (already sent, missing + * RESEND_API_KEY, etc.) + * 401 — auth missing + * 500 — unexpected server error + * + * The page that calls this (web/src/app/welcome/page.tsx) treats any + * 200 response as success regardless of `sent`. Errors are surfaced + * to Sentry, not to the user — a missed welcome email never blocks + * onboarding. + */ +export async function POST(req: NextRequest) { + const user = await getUserFromRequest(req); + if (!user) return jsonError(401, 'Authentication required'); + if (!user.email) { + return NextResponse.json({ sent: false, reason: 'no_email_on_account' }); + } + + const sb = getServiceRoleSupabase(); + if (!sb) { + // Without service-role we can't read/write user_metadata; bail + // softly so the welcome page render isn't blocked. + return NextResponse.json({ sent: false, reason: 'no_service_role' }); + } + + // Read the auth user via the admin API to inspect user_metadata. + let metadata: Record = {}; + try { + const { data, error } = await sb.auth.admin.getUserById(user.id); + if (error || !data?.user) { + return NextResponse.json({ sent: false, reason: 'user_not_found' }); + } + metadata = (data.user.user_metadata as Record) || {}; + } catch { + return NextResponse.json({ sent: false, reason: 'metadata_read_failed' }); + } + + if (metadata.welcome_email_sent === true) { + return NextResponse.json({ sent: false, reason: 'already_sent' }); + } + + // Send + flag. We flag REGARDLESS of send outcome — a Resend + // outage shouldn't queue infinite retries from the welcome page. + // Operators can manually clear the flag if a batch needs to be + // re-sent. + const result = await sendWelcomeEmail(user.email); + + try { + await sb.auth.admin.updateUserById(user.id, { + user_metadata: { ...metadata, welcome_email_sent: true, welcome_email_sent_at: new Date().toISOString() }, + }); + } catch { + // Flag write failed — log via Sentry if wired; the duplicate- + // suppression layer below will eventually catch up. + } + + if (!result.ok) { + return NextResponse.json({ sent: false, reason: result.error || 'send_failed' }); + } + return NextResponse.json({ sent: true, id: result.id }); +} diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index bcd6bc0..c0efd9e 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -11,6 +11,7 @@ import PushPrompt from '@/components/PushPrompt'; import MFAPrompt from '@/components/MFAPrompt'; import MFAChallenge from '@/components/MFAChallenge'; import CookieConsent from '@/components/CookieConsent'; +import SentryInit from '@/components/SentryInit'; import './globals.css'; export const metadata: Metadata = { @@ -109,6 +110,7 @@ export default function RootLayout({ children }: { children: React.ReactNode }) + diff --git a/web/src/app/welcome/page.tsx b/web/src/app/welcome/page.tsx index cca4912..0d6c02c 100644 --- a/web/src/app/welcome/page.tsx +++ b/web/src/app/welcome/page.tsx @@ -2,16 +2,34 @@ import { useRouter } from 'next/navigation'; import { useAuth } from '@/contexts/AuthContext'; -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; export default function WelcomePage() { const router = useRouter(); - const { user, loading } = useAuth(); + const { user, session, loading } = useAuth(); + const welcomeFiredRef = useRef(false); useEffect(() => { if (!loading && !user) router.replace('/signup'); }, [loading, user, router]); + // Session 10 — trigger the welcome email exactly once per mount. + // The server-side handler is idempotent via user_metadata + // (welcome_email_sent flag), so re-triggers from a refresh are + // safe; the ref is just a UX optimization to avoid duplicate + // network requests on this page. + useEffect(() => { + if (welcomeFiredRef.current) return; + if (loading || !user || !session) return; + welcomeFiredRef.current = true; + fetch('/api/welcome-email', { + method: 'POST', + headers: { Authorization: `Bearer ${session.access_token}` }, + }).catch(() => { + // Fail silent — onboarding must not block on email outages. + }); + }, [loading, user, session]); + return (
{ + const dsn = process.env.NEXT_PUBLIC_SENTRY_DSN; + if (!dsn) return; + let cancelled = false; + (async () => { + try { + const Sentry = await import('@sentry/nextjs'); + if (cancelled) return; + Sentry.init({ + dsn, + tracesSampleRate: 0.1, + // PII posture: don't sweep up IPs / cookies automatically. + sendDefaultPii: false, + // Trim heavy integrations we don't need for free-tier volume. + integrations: (defaults) => defaults.filter((i) => i.name !== 'Replay'), + beforeSend(event) { + if (event.user) { + delete event.user.ip_address; + delete event.user.email; + } + return event; + }, + }); + } catch { + // Sentry init failure is never user-facing — degrade silently. + } + })(); + return () => { cancelled = true; }; + }, []); + return null; +} diff --git a/web/src/services/email.ts b/web/src/services/email.ts index e189162..9fc2ca4 100644 --- a/web/src/services/email.ts +++ b/web/src/services/email.ts @@ -75,24 +75,43 @@ const TEMPLATE_HTML_WRAP = (body: string) => ` `; export async function sendWelcomeEmail(email: string): Promise { - const subject = "You're in. Let's grade some props."; + // Session 10 — copy updated to reflect the per-day quota (3 free + // reads/day), Stripe founder pricing, and the live soccer engine. + const site = process.env.NEXT_PUBLIC_SITE_URL || 'https://vyndr.app'; + const subject = 'Welcome to VYNDR — your reads are ready'; const body = `

Welcome to VYNDR.

-

You have 5 free reads every month. Pick a game, read a prop, and see what the model thinks.

-

The books have every advantage. Now you have one too.

-

- Open the slate → +

You have 3 free reads per day. Every read runs your prop through our intelligence engine — the same signals the books use to set lines.

+

Quick start:

+
    +
  1. Go to vyndr.app/scan
  2. +
  3. Pick a player, stat, and line
  4. +
  5. Hit “Read It” — your grade appears in seconds
  6. +
+

When you’re ready for unlimited reads and full reasoning breakdowns, founder pricing starts at $14.99/mo (locked for life): vyndr.app/pricing

+

The World Cup 2026 intelligence engine is live — soccer props are graded with xG regression, altitude impact, referee card rates, and penalty taker signals that nobody else has.

+

+ Read your first prop →

+

See what the market doesn’t.
— VYNDR

`; const text = `Welcome to VYNDR. -You have 5 free reads every month. Pick a game, read a prop, and see what the model thinks. +You have 3 free reads per day. Every read runs your prop through our intelligence engine — the same signals the books use to set lines. -The books have every advantage. Now you have one too. +Quick start: + 1. Go to ${site}/scan + 2. Pick a player, stat, and line + 3. Hit "Read It" — your grade appears in seconds -Open the slate: ${process.env.NEXT_PUBLIC_SITE_URL || 'https://vyndr.app'}/dashboard +When you're ready for unlimited reads and full reasoning breakdowns, founder pricing starts at $14.99/mo (locked for life): ${site}/pricing + +The World Cup 2026 intelligence engine is live — soccer props are graded with xG regression, altitude impact, referee card rates, and penalty taker signals that nobody else has. + +See what the market doesn't. +— VYNDR ${TEMPLATE_FOOTER}`; return send({ to: email, subject, html: TEMPLATE_HTML_WRAP(body), text }); }