From 48d47a266c53f4aca7bce259c1318d642234865e Mon Sep 17 00:00:00 2001 From: Manh Le Date: Wed, 28 Oct 2026 21:17:10 +0700 Subject: [PATCH 01/13] test: characterize Showcase migration contracts --- docs/NEXTJS_MIGRATION.md | 50 +++++++++++++++++++++ package.json | 2 +- test/contracts/fixtures.js | 4 ++ test/contracts/migrationContract.test.js | 55 ++++++++++++++++++++++++ 4 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 docs/NEXTJS_MIGRATION.md create mode 100644 test/contracts/fixtures.js create mode 100644 test/contracts/migrationContract.test.js diff --git a/docs/NEXTJS_MIGRATION.md b/docs/NEXTJS_MIGRATION.md new file mode 100644 index 0000000..b88b11e --- /dev/null +++ b/docs/NEXTJS_MIGRATION.md @@ -0,0 +1,50 @@ +# Next.js migration + +The new application is developed independently in `showcase-next/`. Express remains the compatibility baseline. No live traffic switches automatically. + +## Contract inventory + + +There are 21 business HTTP method/path pairs, including three optional Play Integrity endpoints: + +| Method/path | Next.js destination under `showcase-next/src/app/` | +| --- | --- | +| GET `/` | `route.ts` (302 to `/auth/login`) | +| GET `/auth/login` | `auth/login/page.tsx` | +| GET `/auth/start` | `auth/start/route.ts` | +| GET `/auth/callback/:userFlow/:serverId` | `auth/callback/[userFlow]/[serverId]/route.ts` | +| GET `/auth/complete` | `auth/complete/route.ts` | +| GET `/auth/qrcode/complete` | `auth/qrcode/complete/page.tsx` | +| GET `/auth/qrcode/error` | `auth/qrcode/error/page.tsx` | +| POST `/auth/s2s/signin` | `auth/s2s/signin/route.ts` | +| POST `/auth/mobile/login` | `auth/mobile/login/route.ts` | +| GET `/user/info` | `user/info/page.tsx` | +| GET `/api/geoip` | `api/geoip/route.ts` | +| GET `/api/config` | `api/config/route.ts` | +| POST `/sms/auth`, `/sms/log`, `/sms/token` | `sms/{auth,log,token}/route.ts` | +| POST `/ts43/auth`, `/ts43/log`, `/ts43/token` | `ts43/{auth,log,token}/route.ts` | +| POST `/api/play-integrity/attempt` | `api/play-integrity/attempt/route.ts` | +| POST `/api/play-integrity/verify` | `api/play-integrity/verify/route.ts` | +| POST `/api/play-integrity/token-exchange` | `api/play-integrity/token-exchange/route.ts` | + +Also preserve `/socket.io/` transport behavior, required image/icon URLs used by existing config, and useful 404 behavior. Do not reproduce obsolete JS/CSS library URLs in the new app. Add `/health/live` and `/health/ready` for the new service only. Do not create `page.tsx` and `route.ts` for the same route segment. Use explicit 302 responses for legacy redirect endpoints; Next.js redirect defaults are not interchangeable with Express 302. + + +## Compatibility ledger + + +Characterize these before migration; do not silently correct them: + +1. QR error page currently checks only string `'false'`, not boolean `false` or missing verification. Web session authentication is not gated on PNV. Preserve demo behavior; integrating applications must apply the stricter rule documented in `docs/PNV.md`. +2. SMS/TS43 token failure still creates an authenticated display session. Keep this behavior visibly documented as demo error display, not a new authorization guarantee. +3. TS43 frontend omits `server_id` in `/ts43/token` despite sending it to `/ts43/auth`; characterize the mismatch. Baseline migration preserves it until an explicit behavior-fix decision is approved. +4. `/api/config` publishes callback URLs without server ID, while actual web auth uses both path parameters. Preserve public response shape/value unless a separate correction is approved; new UI builds authorization via `/auth/start`, not this field. +5. Missing callback state generates a UUID; start only checks presence; cached results are not consumed. OAuth state hardening is separate scope. +6. `env` currently depends on `NODE_ENV`. Moving the same deployment label to `APP_ENV` is a required compatibility adaptation, and must be tested in stage. + +No provider call retries or broad timeout changes are introduced. Preserve best-effort CIBA callback behavior. Legacy request-body acceptance includes JSON and urlencoded for existing non-Integrity endpoints; duplicate/malformed query/body cases need characterization. Play Integrity keeps its dedicated 16 KB bound and safe 400 responses even when Content-Length is absent. Keep security headers compatible with consent iframe, browser credential APIs and required external assets. Log request ID, method/path without query secrets, status and duration; do not copy credential/token/PII debug dumps into the new service. Logging redaction and safe React rendering are explicit migration adaptations. + + +## Validation + +Run the legacy suite with Node 24.22.0 using `npm test`. Local provider stubs are not evidence of hosted or real-device success. diff --git a/package.json b/package.json index 5a6c667..e3ae9b2 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ }, "scripts": { "version": "echo $npm_package_version", - "test": "node --test", + "test": "node --test 'test/**/*.test.js'", "start": "node ./bin/www", "dev": "nodemon ./bin/www" }, diff --git a/test/contracts/fixtures.js b/test/contracts/fixtures.js new file mode 100644 index 0000000..fdb6323 --- /dev/null +++ b/test/contracts/fixtures.js @@ -0,0 +1,4 @@ +module.exports = { + client: { user_flow: 'pvn_ip', client_id: 'fixture-client', client_secret: 'fixture-only-secret', scope: 'openid ip:phone_verify', title: 'Phone' }, + server: { id: 'stage', url: 'https://provider.invalid/auth' }, +}; diff --git a/test/contracts/migrationContract.test.js b/test/contracts/migrationContract.test.js new file mode 100644 index 0000000..0859a0b --- /dev/null +++ b/test/contracts/migrationContract.test.js @@ -0,0 +1,55 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const http = require('node:http'); +const express = require('express'); +const session = require('express-session'); +const { randomUUID } = require('node:crypto'); +const { client, server } = require('./fixtures'); +const store = require('../../lib/data_store'); + +async function fixture(run) { + const app = express(); + app.use(express.json()); + app.use(express.urlencoded({ extended: false })); + app.use(session({ secret: 'test-only', resave: false, saveUninitialized: true })); + app.use((req, res, next) => { + res.locals = { clients: [client], auth_servers: [server], realm: 'fixture', baseUrl: 'https://showcase.example', getAuthServer: id => !id || id === server.id ? server : null }; + next(); + }); + app.use('/', require('../../routes/index')); + app.use('/auth', require('../../routes/auth')); + const listener = http.createServer(app); + await new Promise(resolve => listener.listen(0, '127.0.0.1', resolve)); + try { await run((path, options) => fetch(`http://127.0.0.1:${listener.address().port}${path}`, { redirect: 'manual', ...options })); } + finally { await new Promise(resolve => listener.close(resolve)); } +} + +test('migration contract: config redacts secrets and retains callback without server id', () => fixture(async request => { + const response = await request('/api/config'); + const data = await response.json(); + assert.equal(data.clients[0].redirect_uri, 'https://showcase.example/auth/callback/pvn_ip'); + assert.equal(Object.hasOwn(data.clients[0], 'client_secret'), false); + assert.equal(JSON.stringify(data).includes(client.client_secret), false); +})); +test('migration contract: duplicate server and flow query keys retain lookup failures', () => fixture(async request => { + const serverResponse = await request('/auth/start?user_flow=pvn_ip&state=s&server_id=stage&server_id=stage'); + assert.equal(serverResponse.status, 400); + assert.deepEqual(await serverResponse.json(), { error: 'INVALID_SERVER_ID' }); + const flowResponse = await request('/auth/start?user_flow=pvn_ip&user_flow=pvn_ip&state=s'); + assert.equal(flowResponse.status, 200); + assert.equal(await flowResponse.text(), 'Client not found'); +})); +test('migration contract: S2S accepts JSON and urlencoded without consuming results', () => fixture(async request => { + const state = randomUUID(); + await store.set(`${state}-user-info`, { sub: 'fixture-sub' }); + for (const [contentType, body] of [['application/json', JSON.stringify({state})], ['application/x-www-form-urlencoded', new URLSearchParams({state}).toString()]]) { + const response = await request('/auth/s2s/signin', {method:'POST',headers:{'content-type':contentType},body}); + assert.equal(response.status, 200); + assert.deepEqual(await response.json(), {sub:'fixture-sub'}); + } +})); +test('migration contract: urlencoded mobile body follows invalid-client JSON response', () => fixture(async request => { + const response = await request('/auth/mobile/login', {method:'POST',headers:{'content-type':'application/x-www-form-urlencoded'},body:'client_id=missing'}); + assert.equal(response.status,401); + assert.deepEqual(await response.json(), {error:'Client not found'}); +})); From a70954ba661dfe04fef35abf5a2b947518fc7ffb Mon Sep 17 00:00:00 2001 From: Manh Le Date: Wed, 28 Oct 2026 21:29:37 +0700 Subject: [PATCH 02/13] feat: add isolated Next.js runtime and shared auth infrastructure --- showcase-next/.env.example | 8 + showcase-next/.gitignore | 11 + showcase-next/components.json | 1 + showcase-next/config/app_config.json | 78 + showcase-next/config/default.json.sample | 95 + showcase-next/e2e/bootstrap.spec.ts | 2 + showcase-next/eslint.config.mjs | 4 + showcase-next/next-env.d.ts | 7 + showcase-next/next.config.ts | 4 + showcase-next/package-lock.json | 10938 ++++++++++++++++ showcase-next/package.json | 67 + showcase-next/playwright.config.ts | 2 + showcase-next/postcss.config.mjs | 2 + showcase-next/runtime/config.d.mts | 6 + showcase-next/runtime/config.mjs | 40 + showcase-next/runtime/socket-server.d.mts | 2 + showcase-next/runtime/socket-server.mjs | 14 + showcase-next/server.mjs | 18 + showcase-next/src/app/api/config/route.ts | 5 + showcase-next/src/app/api/geoip/route.ts | 5 + showcase-next/src/app/auth/login/page.tsx | 1 + showcase-next/src/app/globals.css | 18 + showcase-next/src/app/layout.tsx | 4 + showcase-next/src/app/not-found.tsx | 1 + showcase-next/src/app/route.ts | 1 + showcase-next/src/components/ui/alert.tsx | 65 + showcase-next/src/components/ui/button.tsx | 63 + showcase-next/src/components/ui/card.tsx | 91 + showcase-next/src/components/ui/dialog.tsx | 158 + showcase-next/src/components/ui/input.tsx | 20 + showcase-next/src/components/ui/label.tsx | 23 + showcase-next/src/components/ui/select.tsx | 189 + showcase-next/src/components/ui/tabs.tsx | 90 + showcase-next/src/lib/utils.ts | 3 + showcase-next/src/server/config/load.ts | 6 + showcase-next/src/server/config/public.ts | 12 + showcase-next/src/server/http/body.ts | 12 + showcase-next/src/server/http/context.ts | 12 + showcase-next/src/server/http/headers.ts | 1 + showcase-next/src/server/http/response.ts | 5 + showcase-next/src/server/realtime/notifier.ts | 4 + showcase-next/src/server/session/cookies.ts | 3 + showcase-next/src/server/session/store.ts | 8 + showcase-next/src/server/shared/logger.ts | 1 + .../src/server/store/auth-results.ts | 12 + showcase-next/src/server/store/rate-limit.ts | 14 + showcase-next/src/server/store/redis.ts | 13 + showcase-next/src/shared/contracts.ts | 7 + .../tests/contracts/bootstrap.test.ts | 9 + .../tests/contracts/config-geoip.test.ts | 12 + showcase-next/tests/helpers/config.ts | 8 + showcase-next/tests/helpers/e2e-server.mjs | 4 + showcase-next/tests/helpers/processes.ts | 27 + showcase-next/tests/helpers/redis.ts | 11 + showcase-next/tests/helpers/server-only.ts | 1 + .../tests/integration/auth-results.test.ts | 11 + .../tests/integration/rate-limit.test.ts | 11 + .../tests/integration/realtime.test.ts | 20 + .../tests/integration/sessions.test.ts | 14 + showcase-next/tests/unit/auth-results.test.ts | 8 + showcase-next/tests/unit/config.test.ts | 17 + showcase-next/tests/unit/context.test.ts | 11 + showcase-next/tsconfig.json | 1 + showcase-next/vitest.config.ts | 6 + 64 files changed, 12317 insertions(+) create mode 100644 showcase-next/.env.example create mode 100644 showcase-next/.gitignore create mode 100644 showcase-next/components.json create mode 100644 showcase-next/config/app_config.json create mode 100755 showcase-next/config/default.json.sample create mode 100644 showcase-next/e2e/bootstrap.spec.ts create mode 100644 showcase-next/eslint.config.mjs create mode 100644 showcase-next/next-env.d.ts create mode 100644 showcase-next/next.config.ts create mode 100644 showcase-next/package-lock.json create mode 100644 showcase-next/package.json create mode 100644 showcase-next/playwright.config.ts create mode 100644 showcase-next/postcss.config.mjs create mode 100644 showcase-next/runtime/config.d.mts create mode 100644 showcase-next/runtime/config.mjs create mode 100644 showcase-next/runtime/socket-server.d.mts create mode 100644 showcase-next/runtime/socket-server.mjs create mode 100644 showcase-next/server.mjs create mode 100644 showcase-next/src/app/api/config/route.ts create mode 100644 showcase-next/src/app/api/geoip/route.ts create mode 100644 showcase-next/src/app/auth/login/page.tsx create mode 100644 showcase-next/src/app/globals.css create mode 100644 showcase-next/src/app/layout.tsx create mode 100644 showcase-next/src/app/not-found.tsx create mode 100644 showcase-next/src/app/route.ts create mode 100644 showcase-next/src/components/ui/alert.tsx create mode 100644 showcase-next/src/components/ui/button.tsx create mode 100644 showcase-next/src/components/ui/card.tsx create mode 100644 showcase-next/src/components/ui/dialog.tsx create mode 100644 showcase-next/src/components/ui/input.tsx create mode 100644 showcase-next/src/components/ui/label.tsx create mode 100644 showcase-next/src/components/ui/select.tsx create mode 100644 showcase-next/src/components/ui/tabs.tsx create mode 100644 showcase-next/src/lib/utils.ts create mode 100644 showcase-next/src/server/config/load.ts create mode 100644 showcase-next/src/server/config/public.ts create mode 100644 showcase-next/src/server/http/body.ts create mode 100644 showcase-next/src/server/http/context.ts create mode 100644 showcase-next/src/server/http/headers.ts create mode 100644 showcase-next/src/server/http/response.ts create mode 100644 showcase-next/src/server/realtime/notifier.ts create mode 100644 showcase-next/src/server/session/cookies.ts create mode 100644 showcase-next/src/server/session/store.ts create mode 100644 showcase-next/src/server/shared/logger.ts create mode 100644 showcase-next/src/server/store/auth-results.ts create mode 100644 showcase-next/src/server/store/rate-limit.ts create mode 100644 showcase-next/src/server/store/redis.ts create mode 100644 showcase-next/src/shared/contracts.ts create mode 100644 showcase-next/tests/contracts/bootstrap.test.ts create mode 100644 showcase-next/tests/contracts/config-geoip.test.ts create mode 100644 showcase-next/tests/helpers/config.ts create mode 100644 showcase-next/tests/helpers/e2e-server.mjs create mode 100644 showcase-next/tests/helpers/processes.ts create mode 100644 showcase-next/tests/helpers/redis.ts create mode 100644 showcase-next/tests/helpers/server-only.ts create mode 100644 showcase-next/tests/integration/auth-results.test.ts create mode 100644 showcase-next/tests/integration/rate-limit.test.ts create mode 100644 showcase-next/tests/integration/realtime.test.ts create mode 100644 showcase-next/tests/integration/sessions.test.ts create mode 100644 showcase-next/tests/unit/auth-results.test.ts create mode 100644 showcase-next/tests/unit/config.test.ts create mode 100644 showcase-next/tests/unit/context.test.ts create mode 100644 showcase-next/tsconfig.json create mode 100644 showcase-next/vitest.config.ts diff --git a/showcase-next/.env.example b/showcase-next/.env.example new file mode 100644 index 0000000..a78272e --- /dev/null +++ b/showcase-next/.env.example @@ -0,0 +1,8 @@ +PORT=3002 +APP_ENV=development +APP_ORIGIN=http://localhost:3002 +REDIS_URL=redis://127.0.0.1:6380 +SHOWCASE_KEY_PREFIX=showcase-next:development: +TRUST_PROXY_HOPS=0 +# NODE_CONFIG_DIR=/absolute/path/to/private/config +# PLAY_INTEGRITY_ENABLED=false diff --git a/showcase-next/.gitignore b/showcase-next/.gitignore new file mode 100644 index 0000000..b6466c1 --- /dev/null +++ b/showcase-next/.gitignore @@ -0,0 +1,11 @@ +node_modules/ +.next/ +.env* +!.env.example +config/default.json +config/local*.json +config/stage.json +config/production.json +*.tsbuildinfo +test-results/ +playwright-report/ diff --git a/showcase-next/components.json b/showcase-next/components.json new file mode 100644 index 0000000..4289c67 --- /dev/null +++ b/showcase-next/components.json @@ -0,0 +1 @@ +{"$schema":"https://ui.shadcn.com/schema.json","style":"new-york","rsc":true,"tsx":true,"tailwind":{"config":"","css":"src/app/globals.css","baseColor":"slate","cssVariables":true},"aliases":{"components":"@/components","utils":"@/lib/utils","ui":"@/components/ui","lib":"@/lib","hooks":"@/hooks"},"iconLibrary":"lucide"} diff --git a/showcase-next/config/app_config.json b/showcase-next/config/app_config.json new file mode 100644 index 0000000..36b480a --- /dev/null +++ b/showcase-next/config/app_config.json @@ -0,0 +1,78 @@ +{ + "config_version": "1.0.0", + "app_config": { + "min_supported_version": "2.1.0", + "latest_version": "2.1.14", + "force_update": false, + "update_message": "A new version is available. Please update to continue.", + "maintenance_mode": false, + "maintenance_message": "App is under maintenance. Please try again later." + }, + + "default_environment": "sandbox", + "branding": { + "app_name": "IPification Demo", + "show_logo": false, + "logo_url": "https://cdn.ipification.com/logos/main.png", + "primary_color": "#1976D2", + "secondary_color": "#424242" + }, + "main_screen": { + "locale": { + "title": "IPification\nfeatures\nshowcase", + "footer": { + "privacy_policy": "Privacy Policy", + "privacy_policy_url": "https://www.ipification.com/legal" + } + } + }, + "pnv_screen": { + "locale": { + "title": "Phone Number Verification", + "description": "Please enter your phone number to continue", + "country_code_label": "Code", + "phone_number_label": "Phone Number", + "footer": { + "privacy_policy": "Privacy Policy", + "privacy_policy_url": "https://www.ipification.com/legal" + }, + "errors": { + "phone_required": "Phone Number cannot be empty", + "phone_invalid": "Invalid phone number", + "country_code_required": "Country Code cannot be empty" + } + } + }, + "ussd_screen": { + "locale": { + "title": "USSD Verification", + "description": "Verify your phone number using USSD", + "phone_number_label": "Phone Number", + "ussd_code_label": "USSD Code", + "verify_button": "Verify", + "footer": { + "privacy_policy": "Privacy Policy", + "privacy_policy_url": "https://www.ipification.com/legal" + }, + "errors": { + "phone_required": "Phone Number cannot be empty", + "permission_required": "CALL_PHONE permission is required" + } + }, + "config": { + "enabled": true, + "ussd_code": "*123#", + "backend_url": "https://ussd-backend.ipification.com", + "timeout_ms": 30000 + } + }, + "global_locale": { + "app_name": "IPification features showcase", + "common": { + "ok": "OK", + "cancel": "Cancel", + "back": "Back", + "info_title": "Configuration Info" + } + } +} diff --git a/showcase-next/config/default.json.sample b/showcase-next/config/default.json.sample new file mode 100755 index 0000000..9e80a84 --- /dev/null +++ b/showcase-next/config/default.json.sample @@ -0,0 +1,95 @@ +{ + "realm": "ipification", + "title": "IPification Showcase", + "auth_servers": [ + { + "id": "stage", + "url": "https://api.stage.ipification.com/auth" + } + ], + "client_id": "", + "client_secret": "", + "play_integrity_verify_domains": [], + "clients": [ + { + "user_flow": "pvn_ip", + "title": "IP Phone Number Verification", + "scope": "openid ip:phone_verify", + "client_id": "webclient3" + }, + { + "user_flow": "pvn_ip_plus", + "title": "IP+ Phone Number Verification", + "scope": "openid ip:phone_verify ip:mobile_id", + "client_id": "webclient3" + }, + { + "user_flow": "pvn_im", + "title": "IM Phone Number Verification", + "scope": "openid ip:phone_verify", + "channel": "wa viber telegram", + "client_id": "webclient3" + }, + { + "user_flow": "pvn_ipificator", + "auth_server_url": "https://ipificator.ipification.com/api/auth", + "title": "IP via App", + "scope": "openid ip:phone_verify", + "client_id": "webclient3" + }, + { + "user_flow": "pvn_sms", + "title": "SMS Phone Number Verification", + "scope": "openid ip:phone_verify" + }, + { + "user_flow": "login_ip", + "title": "IP Login", + "scope": "openid ip:phone", + "client_id": "webclient3" + }, + { + "user_flow": "login_ip_plus", + "title": "IP+ Login", + "scope": "openid ip:mobile_id ip:phone", + "client_id": "webclient3" + }, + { + "user_flow": "login_im", + "title": "IM Login", + "scope": "openid ip:phone", + "channel": "wa viber telegram", + "client_id": "webclient3" + }, + { + "user_flow": "anonymous", + "title": "Anonymous Identity", + "scope": "openid ip:mobile_id", + "client_id": "webclient3" + }, + { + "user_flow": "kyc_phone", + "title": "KYC Data", + "scope": "openid ip:profile", + "client_id": "webclient3" + }, + { + "user_flow": "sample_app_im_backchannel", + "title": "IP merchant sample app", + "scope": "openid", + "client_id": "webclient3" + }, + { + "user_flow": "pvn_sim", + "title": "SIM", + "scope": "openid ip:phone_verify", + "client_id": "webclient3" + }, + { + "user_flow": "login_sim", + "title": "SIM", + "scope": "openid ip:phone", + "client_id": "webclient3" + } + ] +} diff --git a/showcase-next/e2e/bootstrap.spec.ts b/showcase-next/e2e/bootstrap.spec.ts new file mode 100644 index 0000000..796306d --- /dev/null +++ b/showcase-next/e2e/bootstrap.spec.ts @@ -0,0 +1,2 @@ +import {test,expect} from '@playwright/test'; +test('login page is served',async({page})=>{await page.goto('/auth/login');await expect(page.getByRole('heading',{name:'IPification Showcase'})).toBeVisible();}); diff --git a/showcase-next/eslint.config.mjs b/showcase-next/eslint.config.mjs new file mode 100644 index 0000000..b5ad610 --- /dev/null +++ b/showcase-next/eslint.config.mjs @@ -0,0 +1,4 @@ +import {defineConfig,globalIgnores} from 'eslint/config'; +import next from 'eslint-config-next/core-web-vitals'; +import ts from 'eslint-config-next/typescript'; +export default defineConfig([...next,...ts,globalIgnores(['.next/**','next-env.d.ts','test-results/**','playwright-report/**']),{rules:{'@typescript-eslint/no-explicit-any':'off'}}]); diff --git a/showcase-next/next-env.d.ts b/showcase-next/next-env.d.ts new file mode 100644 index 0000000..a419cbe --- /dev/null +++ b/showcase-next/next-env.d.ts @@ -0,0 +1,7 @@ +/// +/// +import "./.next/dev/types/routes.d.ts"; +import "./.next/dev/types/root-params.d.ts"; + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/showcase-next/next.config.ts b/showcase-next/next.config.ts new file mode 100644 index 0000000..905bd9b --- /dev/null +++ b/showcase-next/next.config.ts @@ -0,0 +1,4 @@ +import type { NextConfig } from 'next'; +import {fileURLToPath} from 'node:url'; +const config: NextConfig = { serverExternalPackages:['config','geoip-lite'], poweredByHeader:false, turbopack:{root:fileURLToPath(new URL('.',import.meta.url))} }; +export default config; diff --git a/showcase-next/package-lock.json b/showcase-next/package-lock.json new file mode 100644 index 0000000..afc4882 --- /dev/null +++ b/showcase-next/package-lock.json @@ -0,0 +1,10938 @@ +{ + "name": "ipification-showcase-next", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ipification-showcase-next", + "version": "0.1.0", + "dependencies": { + "@next/env": "^16.4.3", + "@radix-ui/react-dialog": "^1.3.1", + "@radix-ui/react-select": "^2.4.1", + "@radix-ui/react-slot": "^1.5.0", + "@radix-ui/react-tabs": "^1.2.1", + "@socket.io/redis-adapter": "^8.3.0", + "@socket.io/redis-emitter": "^5.1.0", + "axios": "^1.21.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cn": "^0.5.0", + "config": "^5.0.1", + "geoip-lite": "^2.0.3", + "google-auth-library": "^11.3.1", + "intl-tel-input": "^29.5.7", + "jsonwebtoken": "^9.0.3", + "lucide-react": "^1.63.0", + "next": "^16.4.3", + "qrcode.react": "^4.2.0", + "qs": "^6.17.0", + "radix-ui": "^1.8.1", + "react": "^19.3.1", + "react-dom": "^19.3.1", + "redis": "^6.3.0", + "server-only": "^0.0.1", + "socket.io": "^4.8.4", + "socket.io-client": "^4.8.4", + "tailwind-merge": "^3.7.0" + }, + "devDependencies": { + "@playwright/test": "^1.64.1", + "@tailwindcss/postcss": "^4.3.3", + "@types/config": "^3.3.5", + "@types/geoip-lite": "^1.4.4", + "@types/jsonwebtoken": "^9.0.10", + "@types/node": "^26.6.5", + "@types/qs": "^6.15.1", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "eslint": "^9.39.5", + "eslint-config-next": "^16.4.3", + "tailwindcss": "^4.3.3", + "tw-animate-css": "^1.4.0", + "typescript": "^6.0.3", + "vitest": "^5.0.5" + }, + "engines": { + "node": "24.22.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.3.0.tgz", + "integrity": "sha512-U4+70Pc5ZS9osnCBCE5Jha/ciHM+Yp+CNMNC/7HvYbNRk1Ldd+f7qO65W5qfhu/TCv+/ozljlXXe9Nj8419DMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@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==", + "dev": true, + "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/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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.11", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.11.tgz", + "integrity": "sha512-snZbO7MEDOof+hvNa97EjEY7qD7h2kwvQr5AJC3ZhQudKW5dnej6k3kP9NQV88hJwaS8vDqRLFo9rivLEhle2g==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.10.tgz", + "integrity": "sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.11.tgz", + "integrity": "sha512-OZ6+oaK7GMyg3IfCpj8JOmn8n+w2rJbDT0LLfVDTLb1ZFeKDh3AiNELgCMUw175IJHSkLl74hG0M0VLsvx+gag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.11" + }, + "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==", + "dev": true, + "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.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.10.tgz", + "integrity": "sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.9", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.11.tgz", + "integrity": "sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.11", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.7.tgz", + "integrity": "sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.2", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", + "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.8.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT" + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.6.tgz", + "integrity": "sha512-mRyDmEw9/fCDXCGfL1sbgIw/GUK0PVxRW4YwgPN61F3LQ+fSyoBFarJVzgGyhF6kRzEQZ0FejDqm4WLXSeqQmw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || 2.0.0-beta.2", + "@emnapi/runtime": "^1.7.1 || 2.0.0-beta.2" + } + }, + "node_modules/@next/env": { + "version": "16.4.3", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.4.3.tgz", + "integrity": "sha512-a7tz1QCcWWkwC/rPJcPMJk/UNRu9CFhDd3OHYE5hddbJrs8BqRTalxI6GlKCWEGzRLHN+S/B7a4PAHCtm25aBQ==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "16.4.3", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.4.3.tgz", + "integrity": "sha512-oxuC06wt/71h6ACYCV9GbKOfnf5a7VlRe/p80YHTut15TNgDEmynDnaq8t/xhy5rlvk7dhItIPs6H3rvIM4kyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "4.9.1", + "fast-glob": "3.3.1" + } + }, + "node_modules/@next/eslint-plugin-next/node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@next/eslint-plugin-next/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.4.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.4.3.tgz", + "integrity": "sha512-4yq5EXBt+VDtAZwTyfijaFp5D2C5oz3Ikw5a2kiX2K9nw72vrZX2q5UuAlkPtUMO1tQz6JTKpaKWjS7OOefTDQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.4.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.4.3.tgz", + "integrity": "sha512-1Rvkn/LUhnEF8WxnRkOrqGhbwXui1bkWFxONKIbN+r7C+9KHDSZ+iUoj4CzymVMWUyZIyx9szcb9+kbKhD500A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.4.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.4.3.tgz", + "integrity": "sha512-dCKpXErGg3abUM6t5JTJV/ottVBTStJB5lRJTbgdUdQaQ+jNHiK56Hsc0YAEg/rinSh42hYG3HWiYmEMdilnxg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.4.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.4.3.tgz", + "integrity": "sha512-WwYbhWkhYBvBtib1K4W0kL8MW2snBD2A5b3SN4vPpqalZbZs5+bQ1yzAdepVoOqrFfEW3dmREy4Zs1MrWcrFbw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.4.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.4.3.tgz", + "integrity": "sha512-QymWvfUwDEC5/lpJOsxX5VAbyoR2D13+2Jsjz4be7gT4LPqEgdpkeLIR/HFfCLgKzoMhpJbp6I/JnogyNZHcaA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.4.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.4.3.tgz", + "integrity": "sha512-Vmjd9D7DpA4Vv3cUlr82QWgJD/BQZmns801vtC5VWHg6jCIA4a5vMKpQnfrM2pyCrp2eV0ENdUg4PlN3bAc2Yw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.4.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.4.3.tgz", + "integrity": "sha512-lG1pPdPLk7gTyGb00n5JIMLdBJgjb+UEhSdZvFbhUMg0DyC6z4B7RnpSKF9VCd4imkLObVbXOFA0kFCpw7yFow==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.4.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.4.3.tgz", + "integrity": "sha512-NmQC9RkJdnPXRaMxtkCzobB3teVjqF33oSOnwDfi+XYGWWYpt+xlGK3mtktSKLbdjwaWbJu2OkkMiRFGUUncKg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.155.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.155.0.tgz", + "integrity": "sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@playwright/test": { + "version": "1.64.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.64.1.tgz", + "integrity": "sha512-IaYGml/nSOiZWROfPtuIL7neuu9qDOZ3TNvNljUIrhDMxiGTgPjzj/g9p9RxpS2tSepPDRY+UwAaEeiKtUvIAg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.64.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.3.tgz", + "integrity": "sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.2.0.tgz", + "integrity": "sha512-KABY+VMIRPJL+Vyzv/335FEhXXD/xDv8BMb1kl48/aXJZMH0LtHCTx5ga5RnxaEP3nZHtmTLf1P9vanmFX46bw==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accessible-icon": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.2.0.tgz", + "integrity": "sha512-BB4+HHkFl3HipEAhm/mLhrpXV+2kRX7MvY4AaImUcQVeWQt5KhV1JEByNs8NGfJOcr95RcQGijt9+OrCo68xGA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-visually-hidden": "1.2.13" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.3.1.tgz", + "integrity": "sha512-lCZ6sbGS/7WO0j84yx4srIgOf0MRJ/sZ13RonADXxvKBym8xrxNCq5f6DibGcr38sIlwPk8/52uYdXeOPsbEDw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-collapsible": "1.2.1", + "@radix-ui/react-collection": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-direction": "1.2.0", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-controllable-state": "1.3.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.2.1.tgz", + "integrity": "sha512-MWC4N7RY+qsbNrFNo7yQ9ZbdoZEW+B5DtpWxJfUnVCqcQxxqqg3qis3RMPTDbCNaTIdHJSn5XDkl9ZdVWXknLg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-dialog": "1.3.1", + "@radix-ui/react-primitive": "2.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.17.tgz", + "integrity": "sha512-67jjcVvxjXCCStnTW3Z+M7jmaFMuMR7M2JgpDzH4w7JSRrikmzhjfYgP/09ECb8n7whnsgh1ifJ+Ie96qEj+pQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.2.0.tgz", + "integrity": "sha512-gHEU+Lx8DlT/FYIjZQvvBJrFSwUngXTjh7FeL9QvJaNALDLez6pgDj2axCUihBjtPBy82LN0gNb25+sTcSXNmA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.3.1.tgz", + "integrity": "sha512-ssZOkfQNkFpYVmCPQlr978xSaU7moSrGXdwg4j2Xs2WlGl/1KsSZtfpK+WnLnx0Rwll1BAh6oUrAy/uR31SBvQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-callback-ref": "1.1.5", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.4.1.tgz", + "integrity": "sha512-KWtwhh+98ML3seZ9vO9yRY2OwaQG90dCiDAxjpSNY2AaZ3Hik3B0dsk6m0ILywo/no3A9/SozadyPMNweVT8yw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-presence": "1.1.11", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-controllable-state": "1.3.0", + "@radix-ui/react-use-size": "1.1.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.2.1.tgz", + "integrity": "sha512-fSzLb/qy1wuZSxQBcxpW0hNXYHDMWiIXDGyJGdA4+FSLknOWZsNRrTCUavxb4PI8b8lvHo/CysO6NVi5zLpo5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-presence": "1.1.11", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-controllable-state": "1.3.0", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.2.0.tgz", + "integrity": "sha512-+yiQzqcc7Sy7/5s0jrMCjpPRFB3538VNrXPWdTLZhE/SCfgEws4aI6lHWN/vvcnyhHndROWBzqmqB4QBrITZ9Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-slot": "1.5.0", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.5.tgz", + "integrity": "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.3.0.tgz", + "integrity": "sha512-v5AYEocMISkbJ+1fTXzZpiFv7NRr3D2WiYbLIKf+FkmJ8VkP6d503eV2n34MFussHXY1pcoq3yfJiQH77dc4Mg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.4.1.tgz", + "integrity": "sha512-Pddn3Z8lio3/ChxyXaEP3jHTTo1LTHCWwF8+mG1chQlWijP+ttVHBKwezrQTiV6Mj670ojvumqvuGh+mdv33iQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-menu": "2.2.1", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-controllable-state": "1.3.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.3.1.tgz", + "integrity": "sha512-+0E4qvVTwjAWra9sEhJ+9BTsvHE00zkUuNw1VGh7S+Gc13dfYk+S1PVQdchWDj+XYHhAai7Z7IpzXVDCGsDplg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-dismissable-layer": "1.2.1", + "@radix-ui/react-focus-guards": "1.2.0", + "@radix-ui/react-focus-scope": "1.3.1", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-portal": "1.2.0", + "@radix-ui/react-presence": "1.1.11", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-slot": "1.5.0", + "@radix-ui/react-use-controllable-state": "1.3.0", + "@radix-ui/react-use-layout-effect": "1.1.4", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.2.0.tgz", + "integrity": "sha512-FtRA52a/r+LKStFT+neLUm+8vSe7wbzkbQGGeU8ixFiwC8NU14Evoq9ATECqL3eGbcUzJR4fyfA4KkK/q6dfqg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.2.1.tgz", + "integrity": "sha512-KIfDcAsx+oVylFZ4xryVKusJUrLLhTQmfnJW4kwk4Km5myvhHCrj04hnWWUC166+xD4DHdH8tQGylwYX1h6+Pg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-callback-ref": "1.1.5", + "@radix-ui/react-use-effect-event": "0.0.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.2.1.tgz", + "integrity": "sha512-XsHcmYG2AcqnvTqrOXXWcMrhquEIlqucizuDH1MV/qMRW+DndpBCMvNeTRXOxqmG1H5j89M/+I0/34Zdd+zDcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-menu": "2.2.1", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-controllable-state": "1.3.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.2.0.tgz", + "integrity": "sha512-MPm5R0iRGUDrDnPNxmLBkv+X7pN4XKzt+MbWJEuJQsH3NFH4omrbpXpucyYCCKTSt+na/ZxGhEWQrWTZ0285cA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.3.1.tgz", + "integrity": "sha512-ugeMhbXt4y5S2T1GnIqbWDKEN962PDwnX1fN0eKOOgf5f63Dh2mn8F1FUySlgW0iF6GiVsRF32DzuDIWTUyQJg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-callback-ref": "1.1.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-form": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.3.0.tgz", + "integrity": "sha512-KnmrCM27m7Jo1R8S6cFDPrEeq20Jfn5dqde0SZrJr2lVXlj6/QZSUCyFoYh1d8ti0uUrMNK5Lx6dUDuINT+f4Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-label": "2.1.17", + "@radix-ui/react-primitive": "2.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.2.1.tgz", + "integrity": "sha512-4QooHxgmpCKRjj1Pft+hETKidYcrbPKcVwMGtk2Oa5DcWY2IH1fHuQt9LG6q5hUQBBV2MhQxSvtx6B/C1xfiTw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-dismissable-layer": "1.2.1", + "@radix-ui/react-focus-scope": "1.3.1", + "@radix-ui/react-popper": "1.4.0", + "@radix-ui/react-portal": "1.2.0", + "@radix-ui/react-presence": "1.1.11", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-controllable-state": "1.3.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.4.tgz", + "integrity": "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.17.tgz", + "integrity": "sha512-xmj7MN/drx1oUAXkfiFIvyDmGNUIN/omGobvA5oAoNqu9zO2MkISYGZOkBcqSzs/UZXXwV+/3ACRE7q7mGcmkA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.2.1.tgz", + "integrity": "sha512-G34BsU90SBj5foYpz8pQqq/Q0Dlfyyl6KitWEfRwXC0QvfwhWNAwS49DWP2+rRJ0iwv20Z2Qx7Sk+T03bGo5JA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-collection": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-direction": "1.2.0", + "@radix-ui/react-dismissable-layer": "1.2.1", + "@radix-ui/react-focus-guards": "1.2.0", + "@radix-ui/react-focus-scope": "1.3.1", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.4.0", + "@radix-ui/react-portal": "1.2.0", + "@radix-ui/react-presence": "1.1.11", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-roving-focus": "1.2.1", + "@radix-ui/react-slot": "1.5.0", + "@radix-ui/react-use-callback-ref": "1.1.5", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menubar": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.2.1.tgz", + "integrity": "sha512-bRTYtS9JITV2dUYqYZb7+N5SooQBkZrNRejfmyrSKe3Y57aXhKgx//kBUP0kY7pEpsWR/U1AHsAJRzjwCrK3PA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-collection": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-direction": "1.2.0", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-menu": "2.2.1", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-roving-focus": "1.2.1", + "@radix-ui/react-use-controllable-state": "1.3.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.4.1.tgz", + "integrity": "sha512-XPPZjkeyCSMNNvt0FyDv/PUauG7Ow4GwHQ/XZSwW36F/TS7rR4HFTsXH8ewLnXTOLn3x96/pmTPNQIVoTX/86g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-collection": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-direction": "1.2.0", + "@radix-ui/react-dismissable-layer": "1.2.1", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-presence": "1.1.11", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-slot": "1.5.0", + "@radix-ui/react-use-callback-ref": "1.1.5", + "@radix-ui/react-use-controllable-state": "1.3.0", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-previous": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.13" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-one-time-password-field": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.2.1.tgz", + "integrity": "sha512-KtHdcgnaAK6rohQ/MMJoGYcbgSRJsIuGBdHrFeTB1/9SaKlDs14oSOniW6BL6M43HqvgCO3u4WuVdFLkuc6sqw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-collection": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-direction": "1.2.0", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-roving-focus": "1.2.1", + "@radix-ui/react-use-controllable-state": "1.3.0", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-password-toggle-field": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.2.1.tgz", + "integrity": "sha512-vu4+ZoJAaFjdI7l1lDSZRs+4dEv83Jmu7lyzajPrAeQzX74y84n0rUNWbWSgN9XB5NE8IY86Z02kGH6oRrzR+w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-controllable-state": "1.3.0", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-is-hydrated": "0.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.3.1.tgz", + "integrity": "sha512-ZNxvXRx5W2Wos4fXCXb5lpdqN0Z5chf+mStOUOTr4ik7d+62MRvoe9rzCA/eGNg36lxXqSrKEKmwuZEcUrIstw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-dismissable-layer": "1.2.1", + "@radix-ui/react-focus-guards": "1.2.0", + "@radix-ui/react-focus-scope": "1.3.1", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.4.0", + "@radix-ui/react-portal": "1.2.0", + "@radix-ui/react-presence": "1.1.11", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-slot": "1.5.0", + "@radix-ui/react-use-controllable-state": "1.3.0", + "@radix-ui/react-use-layout-effect": "1.1.4", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.4.0.tgz", + "integrity": "sha512-yCWRvLeGv9Pz3+efwHqGPp70mZ56r3kfIgQVF56Oj2aAa4uP/Tb+IE59wNcc+758rctqbCKx4LbK8II/r7Bn0Q==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.17", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-callback-ref": "1.1.5", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-rect": "1.1.4", + "@radix-ui/react-use-size": "1.1.5", + "@radix-ui/rect": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.2.0.tgz", + "integrity": "sha512-StX7xXnaRU41VvFtMm9zlhVGz8XF6GmWWTXftkQaY+tqVkbs/R9UmAp5mLzHHapk5hM9RMZQvO/3usyIP/g42g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.11.tgz", + "integrity": "sha512-Z7/atlkab36Z8hm+V8zYc79r0W+tiOB1o0tun1By94JyadkekLQmtEAnBSP9XKXBNQsStoStS4+gX10JpS5akA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.2.0.tgz", + "integrity": "sha512-HCTSPMNqv55Qk0IX4geJbpyXazmqulcimwuyjxTkKi29v+QZbG2Zho9BnbBq4YS2PF+KlNb2CXoLz66uUmblkg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.5.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.2.1.tgz", + "integrity": "sha512-yQBiM6J3YM8Ums1orkEFpbznK47geKxa0igikzcaS1xHgdba3DdAQBsGbCTt5BVfR8zhEvgxQ0poGcINaD3lvw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-primitive": "2.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.5.1.tgz", + "integrity": "sha512-QbyVT1ddnjqB9H+CUYKkNWdU9fIK3QHCYtjf7mS6ekSqiuXkWtWn5UZW9mhA8qtl3vrVyIsmR9gePstAtKrqkw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-direction": "1.2.0", + "@radix-ui/react-presence": "1.1.11", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-roving-focus": "1.2.1", + "@radix-ui/react-use-controllable-state": "1.3.0", + "@radix-ui/react-use-size": "1.1.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.2.1.tgz", + "integrity": "sha512-UwraORzVUeEPMclhO1p0vz0qI+TbdDLO9i+FiOqhJyHKwdpCRCck9yVEscUnAMIzCiuNoQa8D4A9bIpOPXGt6Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-collection": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-direction": "1.2.0", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-callback-ref": "1.1.5", + "@radix-ui/react-use-controllable-state": "1.3.0", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.4.0.tgz", + "integrity": "sha512-CFrLqHNejWq5FhKAcj9V/WPGRsol76HBQto81NdOBVFEOqIEkJkKIJt0Z824A/9hHLCtoiR/Kw3euRgzXP2pxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-direction": "1.2.0", + "@radix-ui/react-presence": "1.1.11", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-slot": "1.5.0", + "@radix-ui/react-use-callback-ref": "1.1.5", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.4.1.tgz", + "integrity": "sha512-888ZwW0Md6GyptuqYABRX9nLKgoYfcooCyBBvVaX5Jp3e+8Jp2i4Zo/Uy50Hm+Ysn2JfloQXstW4U36w8teVtg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-collection": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-direction": "1.2.0", + "@radix-ui/react-dismissable-layer": "1.2.1", + "@radix-ui/react-focus-guards": "1.2.0", + "@radix-ui/react-focus-scope": "1.3.1", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.4.0", + "@radix-ui/react-portal": "1.2.0", + "@radix-ui/react-presence": "1.1.11", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-slot": "1.5.0", + "@radix-ui/react-use-callback-ref": "1.1.5", + "@radix-ui/react-use-controllable-state": "1.3.0", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.13", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.2.0.tgz", + "integrity": "sha512-1jUAap0wZEqVP05Ou7kJgRxe/W+Qftng+rUNWGa0qkKbv9r5pWTG3E7a3ELKo2eJoHKMECnzuYrZpSyO1YgcYQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.6.1.tgz", + "integrity": "sha512-IRNRR90moPqxfwCJ0XbMJIxo70W0/1ZExT/sNCxEHMaPx0SF9HoQrcPEC75+H28cFKa0b+KH63anfLYJmwKacw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-collection": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-direction": "1.2.0", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-controllable-state": "1.3.0", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-size": "1.1.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.5.0.tgz", + "integrity": "sha512-tyYd9ddPXAysAbdbD9eYxgwpCSv6f3PncqVSNKQFSR5lcJVLz3cb/pANDoYrLiHBpXv7skortyUgGUWsYuPPFQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.4.1.tgz", + "integrity": "sha512-NpBGwc40rY02ZmSvuyeKk0Bvc8ZsNeTMi4qHvJ+ZH4EiYCv0zIXEgdCW4bVdzgIUeP9xxbLJSxKOUfqITYNmkA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-controllable-state": "1.3.0", + "@radix-ui/react-use-size": "1.1.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.2.1.tgz", + "integrity": "sha512-hRoJlode+hg5ZNlR+3tkfkUliIybdTpD5lkAvHQ9yaXseGf7XVO9Ib2GPEIOuaJJbo7YWYbNpDDRfo8Dhf6/xA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-direction": "1.2.0", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-presence": "1.1.11", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-roving-focus": "1.2.1", + "@radix-ui/react-use-controllable-state": "1.3.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.3.1.tgz", + "integrity": "sha512-iQvL0J7XK4HtJNGkkWMyYrboNseI7vtotFeUkIno8YNxVA9oM7B6//fh0Mc6TSKI96+zioLWi98lQLso88yJmQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-collection": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-dismissable-layer": "1.2.1", + "@radix-ui/react-portal": "1.2.0", + "@radix-ui/react-presence": "1.1.11", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-callback-ref": "1.1.5", + "@radix-ui/react-use-controllable-state": "1.3.0", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.13" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.2.1.tgz", + "integrity": "sha512-+3MUq+WyiBplXYqQ0EIYlq8R4sLQ0T5NbOK7DMdDLCKO93pe2uE0DUJTWWfTBprpJJXxfoF3eMW0xpakVZrYsg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-controllable-state": "1.3.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.2.1.tgz", + "integrity": "sha512-O8Ldx5ao4p4KoW+DO8/PmkOe5mdKNi3F2SseqjVj88wvDGDHe60cUmO0Zm/uHsKrvNgQGV8Efpuh95Tht/h4vw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-direction": "1.2.0", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-roving-focus": "1.2.1", + "@radix-ui/react-toggle": "1.2.1", + "@radix-ui/react-use-controllable-state": "1.3.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toolbar": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.2.1.tgz", + "integrity": "sha512-Cm8m7JTEeUdbfA6T9xgyu8HfOpyvSx8pB/wj0DlEuWaJe9FilIs4Az9wci/lU9RbthV/UkltszMw5WnnD+m/Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-direction": "1.2.0", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-roving-focus": "1.2.1", + "@radix-ui/react-separator": "1.2.0", + "@radix-ui/react-toggle-group": "1.2.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.4.1.tgz", + "integrity": "sha512-NxLFGSFtZ78zVfcoOhTwR1xwqo5qx1Jh8Pw6FMZ9McCaqfRzz7/gIRahs2Oy5XlPgQ+8QkUcKbN5B0U44J/kCg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-dismissable-layer": "1.2.1", + "@radix-ui/react-focus-scope": "1.3.1", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.4.0", + "@radix-ui/react-portal": "1.2.0", + "@radix-ui/react-presence": "1.1.11", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-slot": "1.5.0", + "@radix-ui/react-use-controllable-state": "1.3.0", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.13" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.5.tgz", + "integrity": "sha512-Y5GBf4NKeADoXI1geiMlMuMoURT3k9MizzzKhGb/m55H94aGigZ01PIpkvsO6lML13BA6XYAXSJ6gza7vTn5nw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.3.0.tgz", + "integrity": "sha512-TAcBI7bw2ljYCbv4+zuTSd6zICu8pRt6SDN2vJ/Z06K2MXh6I9WYFe/A9ovlFDtR5HVHGm5iDxDoABm/++dbKg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.5.tgz", + "integrity": "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.6.tgz", + "integrity": "sha512-9cRullnQRv+kqnRmFlmh0WC3kAPuGmHiQ9VwsXdrYk//sBhgCKEttcpPEPgIQbyx6TGUmooVEHYTyohwz/lSJg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.5" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.3.tgz", + "integrity": "sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.4.tgz", + "integrity": "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.4.tgz", + "integrity": "sha512-XoSLhbRbqxFtgJoi2fNHA3C6pDlY34x508vUpUGoFZfvePfHXHbE1lC4FYFMnJWgiCRroSTw6fOsXQoVS9RwZg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.4.tgz", + "integrity": "sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.5.tgz", + "integrity": "sha512-V1McDQkMGzgivwqdu7u96APs7oi0Jvj9LTzgq3WWrDZqxWdAqU9yZnZQqk5OizGFR8badUQwZsrlao7MOlWYOA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.13.tgz", + "integrity": "sha512-feijiN1oPk4rck4u9Dm9tsU4mT6Wp9oeEEhaz1WUEs1VuqLf8xzsFG+Qk+uWq6TnMH9O+WRECRg0NftyP9E4Fw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.3.tgz", + "integrity": "sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==", + "license": "MIT" + }, + "node_modules/@redis/bloom": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-6.3.0.tgz", + "integrity": "sha512-NQ5poYpZr0jv6zazN1cy6JY5kIipKFZ3R9GTL4kOUTX3a9YAfY29U8bsE4lWhVQEM+G4V0BXVC57mubIdDijeA==", + "license": "MIT", + "engines": { + "node": ">= 20.0.0" + }, + "peerDependencies": { + "@redis/client": "^6.3.0" + } + }, + "node_modules/@redis/client": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-6.3.0.tgz", + "integrity": "sha512-fHrjwGCBvAANGi+TuRuwKi83sIOzgHr8eh+Uf48w3fSX7Hhrz174NrWqfJhAC2vnN0d/tfaZpdr6+o6JC3QHwg==", + "license": "MIT", + "engines": { + "node": ">= 20.0.0" + }, + "peerDependencies": { + "@node-rs/xxhash": "^1.1.0", + "@opentelemetry/api": ">=1 <2" + }, + "peerDependenciesMeta": { + "@node-rs/xxhash": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + } + } + }, + "node_modules/@redis/json": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-6.3.0.tgz", + "integrity": "sha512-yt6vRfPBXtW/3qm53cF81TSQpLMjWdW4/2E9WQ2GgbvsKkuCKCFQIcMiKlil1ZMnezK+IQuS795xKU5psbaegw==", + "license": "MIT", + "engines": { + "node": ">= 20.0.0" + }, + "peerDependencies": { + "@redis/client": "^6.3.0" + } + }, + "node_modules/@redis/search": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-6.3.0.tgz", + "integrity": "sha512-RPXAZmKjZZG7Tm7m1w9keFdXzbL91PiSHQBfQRe9xwW40jNsTRbG0FwhIz4QlHPU4gKDAsCy8IQHj4yZl4H9rw==", + "license": "MIT", + "engines": { + "node": ">= 20.0.0" + }, + "peerDependencies": { + "@redis/client": "^6.3.0" + } + }, + "node_modules/@redis/time-series": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-6.3.0.tgz", + "integrity": "sha512-+fJOB8mN1z5WRakCJNh5lVdz1KqvHGSOistsKeitD24QsCtPwyRQXWC0iq6T/i/ubLE/YcguZqPm8F598MKk4A==", + "license": "MIT", + "engines": { + "node": ">= 20.0.0" + }, + "peerDependencies": { + "@redis/client": "^6.3.0" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.15.tgz", + "integrity": "sha512-aLcgK9gIJ3YRvrrtfSBbRa27F5WrpiTaPulnFpsHPxpUHgFlSgnU4854MHwoRGEBhfRJDWCS2WQ7lIjskE9Olw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.15.tgz", + "integrity": "sha512-xD7DdZCrbThKuxNg7ooQmvp07ys4J/pnSEMTG3/OdOWl6daC67m/Q4fL78sRHMxTH/N4fGUbYhcPM0MF5WFHkg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.15.tgz", + "integrity": "sha512-m2WuQ6r0B4OTYxyPpNLtXaMRbRDUbG8YQV2Q3KTdSSK8Th6KHCnC8jVCJt8bvYeSB5WivkkNbc85mKadaSdOxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.15.tgz", + "integrity": "sha512-5RtnsHM36nodM9WbMy+zip0LGS/G+y5wro5RHrpOlr4ac24+1zSi9kiV2MP1myZeZ/ifEHfulQcWjnAXzc2C2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.15.tgz", + "integrity": "sha512-g3l09pyFqhu+135JKkbkwiKGQmwU5hUMUPJ4VdBADvuqR8d33L6H578rh8m7rtxbdp+VmX/+nzI7nvhSZlzpUQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.15.tgz", + "integrity": "sha512-nIe3VugI97nGQtspPxDqA4sXMUpeiocKIP/CqpY/u/vgbT+alX+cVxu1C/oVBPKg4feNTmYovE91v4DgXvXNwQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.15.tgz", + "integrity": "sha512-iTBjXx+B5QSvgI89lJfmqh4wTh8NabR1YZIIN3hNx4zSG0EVfVqkOvLulNpG4251pbd6/IAmZ/LefVk1Y4rPhw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.15.tgz", + "integrity": "sha512-uN26XOnVR7rqQupG3V9oAG2EGGatWjcJHOeDOHUFCh4TBWmP2tHTFoVN13aYbwfm0aHqJAEmcwhAu6sooIjfqQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.15.tgz", + "integrity": "sha512-9nWkcdd+H+XhaA3fQIwpHnKIrfx/PQ6rO0pivr78Og07tXlUlctBBJoEYLPfhTmCR/QxEVdn6uPjHV5SymPhdw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.15.tgz", + "integrity": "sha512-Ggh0ciFjLwIusLSLz7LZYEaGdxOAUJXI+Qu5JCbhPJJHEt8vP+IjRATTJheiJsdzzTYgUiHUiw3X1Ea3ZpMBdA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.15.tgz", + "integrity": "sha512-FHg60rS6DBePAYZ9X9F67x+JObFdmgn6tupA1C/a3B+CufFmTD4mX+e8M75tCiAO3foR/6xwVBI0KQiI7sn/rw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.15.tgz", + "integrity": "sha512-/cvV/7v5VGz/moiz0XKRj2dE8DzsKyhIu1KpOvJGPjRywg3yscON889UGvAe8zfiJLHup7VLjBduoGWi3HusWQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.15.tgz", + "integrity": "sha512-MyzC5Hrv/sm+Z7TSGKP/j8001RO2xQm1zOpCNVL4j0cY/VhBZviludA2YFQ7i4lH6ZAXxQm0lcMdBzM+99mE7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.15.tgz", + "integrity": "sha512-+PgYiS+AH5omy6L45ZLxybasauJaj3M8buoT83D+nzPmCayXlxUV9hydhciCKVyIg0B/v0BtXPDE1uGqNwCPpg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.15.tgz", + "integrity": "sha512-HFZd2QNQYn1JbIi+Hpd7H+r/lkWxcfIu28nirZ6eCn2LafuvmExMlb/P96Sfx5tf7pPArw39IdhmCF8vmV+NhQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@socket.io/redis-adapter": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@socket.io/redis-adapter/-/redis-adapter-8.3.0.tgz", + "integrity": "sha512-ly0cra+48hDmChxmIpnESKrc94LjRL80TEmZVscuQ/WWkRP81nNj8W8cCGMqbI4L6NCuAaPRSzZF1a9GlAxxnA==", + "license": "MIT", + "dependencies": { + "debug": "~4.3.1", + "notepack.io": "~3.0.1", + "uid2": "1.0.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "socket.io-adapter": "^2.5.4" + } + }, + "node_modules/@socket.io/redis-emitter": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/redis-emitter/-/redis-emitter-5.1.0.tgz", + "integrity": "sha512-QQUFPBq6JX7JIuM/X1811ymKlAfwufnQ8w6G2/59Jaqp09hdF1GJ/+e8eo/XdcmT0TqkvcSa2TT98ggTXa5QYw==", + "license": "MIT", + "dependencies": { + "debug": "~4.3.1", + "notepack.io": "~3.0.1", + "socket.io-parser": "~4.2.1" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.3.tgz", + "integrity": "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "postcss": "^8.5.16", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.4.tgz", + "integrity": "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/config": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@types/config/-/config-3.3.5.tgz", + "integrity": "sha512-itq2HtXQBrNUKwMNZnb9mBRE3T99VYCdl1gjST9rq+9kFaB1iMMGuDeZnP88qid73DnpAMKH9ZolqDpS1Lz7+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/geoip-lite": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@types/geoip-lite/-/geoip-lite-1.4.4.tgz", + "integrity": "sha512-2uVfn+C6bX/H356H6mjxsWUA5u8LO8dJgSBIRO/NFlpMe4DESzacutD/rKYrTDKm1Ugv78b4Wz1KvpHrlv3jSw==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.6.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz", + "integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@types/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-67MQl+fpWKVTT1NYdnmo3U4sc/xPo/zQBncVnI74qmQa0z/b+1g6iYqNmGCPbxO+zz2aklb08a0oHfegiVd0/w==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.72.1.tgz", + "integrity": "sha512-5CokTWXpmbylfFabsbvHC0CAw3XLaGnoa+nFJX5FR592h/LoZsENUE6p/rg5xvkKrFEXz6pYmgI29AgXc2N3BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.72.1", + "@typescript-eslint/type-utils": "8.72.1", + "@typescript-eslint/utils": "8.72.1", + "@typescript-eslint/visitor-keys": "8.72.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.72.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.14.tgz", + "integrity": "sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.72.1.tgz", + "integrity": "sha512-ZGwZP4OCfyA0AQTpEAMWz+rtX01DRhQWW5hB0ucKDEIX5U7jdmPSaMx94rQfaPAQzIYz2BimOgKOJtjfpRFQ9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.72.1", + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/typescript-estree": "8.72.1", + "@typescript-eslint/visitor-keys": "8.72.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.72.1.tgz", + "integrity": "sha512-fAuVMkwdO/huI8nyo/WKnNKf6Gag93S56Rh3Ts5mGcavwSdaN8tRg1gCeIGX8PF+nzp1BjGfO4gHhGzW1J6jTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.72.1", + "@typescript-eslint/types": "^8.72.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.72.1.tgz", + "integrity": "sha512-lC/t6eMCZZQAAKZEYGL6RDVSuFehpdmMT6h+nJIjIa6oylN/PwDgDVtEIbB7O9xFfZYtZAYLoLlDNWKJhtF+uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/visitor-keys": "8.72.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.1.tgz", + "integrity": "sha512-e4D5R5vpeAEPBxT2BAzsFQL3sbwyeeeo5+aczgNSRK1V734jE7pTqaGFU+fXapWEQwnZbpFaTn32+Z8DwS6OTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.72.1.tgz", + "integrity": "sha512-iY2wlmyrXJmx4yxUGP3aYCFF3cA/6ShddOKmLU2dx7LnEykbAM8WThlxZEfd1NddqceF6eJmZUayxx9oKIYNlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/typescript-estree": "8.72.1", + "@typescript-eslint/utils": "8.72.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.72.1.tgz", + "integrity": "sha512-LmE5IhQet1BWOWEXqBrVtBPsMAOQ6Nzk35d+WLh3mk4EUF33sqlfxjio1MdYeSvKSG5skactKKuixBIKXhL4Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.1.tgz", + "integrity": "sha512-DAlUoUV93hIvrtIQLwCbmZQYVZwOMhiHJrOVUUIWicFZaIYjTRqlY3I/NZevfkLmSqkNHRu2qYDcq9lrn16oOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.72.1", + "@typescript-eslint/tsconfig-utils": "8.72.1", + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/visitor-keys": "8.72.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.72.1.tgz", + "integrity": "sha512-5v5liypurJBFHCBNqh/RjsymAD+VWcE0djuGY/BUqp5fC3Qn+luWDEksjuTtWjPn6i5zQlUtkZCRG/CYG0yCkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.72.1", + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/typescript-estree": "8.72.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.1.tgz", + "integrity": "sha512-SB8xqipSzZHUePCCLNeYxqnxq83X61/qzNhjQGbTfjxGZIeOEP7tBgWYX05Tcd6CgE02WdTJnh5b8kMZJ/+bnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.72.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz", + "integrity": "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.12.2.tgz", + "integrity": "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.12.2.tgz", + "integrity": "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.12.2.tgz", + "integrity": "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz", + "integrity": "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz", + "integrity": "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz", + "integrity": "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz", + "integrity": "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz", + "integrity": "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz", + "integrity": "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz", + "integrity": "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz", + "integrity": "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz", + "integrity": "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz", + "integrity": "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz", + "integrity": "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz", + "integrity": "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz", + "integrity": "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-openharmony-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz", + "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz", + "integrity": "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", + "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", + "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", + "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vitest/mocker": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.5.tgz", + "integrity": "sha512-xLL3lkDegJZw+MGoGaCCsFayDZthGgpAtr3Vziyfc4p7xH89K7KRPcRBBh/kEYm0Aqkt5DtTmkYpBtRV/FrHZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.31", + "@vitest/spy": "5.0.5", + "estree-walker": "^3.0.3", + "magic-string": "^1.4.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9 || ^3.0.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/magic-string": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.5.tgz", + "integrity": "sha512-oLLC2+YvQUfAa+vxoufE/yc1icb87M9iXIRqAoR/2EhE/8cb3SF/+Gi0c2By1B+5Vz2e2vQtUvHiu1+63Znh+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/@vitest/spy": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.5.tgz", + "integrity": "sha512-lwJRExuH417257Q5wv9QYugN9IMJkoxcrKrYxGPQkBzl7wlNpTUxD/5yNX4ESKVUIShKPgkGchYPFMWOOk0wzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.2.0.tgz", + "integrity": "sha512-VXY5eFRarnXcYxwBjJzPmEhH55+rmP79/+ueDhi0F+TuqfHCItagIHqxeUZrmgrOPa31QTh9H85DjX3FfJ0FTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "es-shim-unscopables": "^1.1.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.14.0.tgz", + "integrity": "sha512-9WTZxEjsZ7b13TH8JPmbV2z8CHbl80/2hm3XPEG4JgNdQLK81IBRXmSxHfMAOkSqQeRxT/0dwNDz2GOm3zzpcQ==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axios": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.21.0.tgz", + "integrity": "sha512-YCzXMB53Q9PZBsh7yN7wtapit4WepwcHS2fP8QhGsBxSz5YtgYobV7GGlWlAJi+RxfU77GQTQi2DSLUma42EYA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.34", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.34.tgz", + "integrity": "sha512-Aqr8mA1kIvyJ3UY5YAY7ib8LSo1DmDMbMnVqGPtsKW1LxaFQFwn9Eb3yvq2fUKJsRvaXjnAV7R5osqETapzNiA==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.29.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.5.tgz", + "integrity": "sha512-khTmKu5NGYTotqcYo8Bew2k4RAE9PuXRSFRXG2lQBrCEQHyeQ/vX6gKMxfokwywBsLiXZMnHsmWMG1pnO8J7uQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.33", + "caniuse-lite": "^1.0.30001819", + "electron-to-chromium": "^1.5.464", + "node-releases": "^2.0.60", + "update-browserslist-db": "^1.3.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001819", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001819.tgz", + "integrity": "sha512-LFUqIIAwpzsIuevnKnsR9qnStBcgzPShTX61lBwN9uFkkf8o8zLiebEjbW8AYtZiF+1YKDWfdt/pWDtMuG8Xwg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cn": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cn/-/cn-0.5.0.tgz", + "integrity": "sha512-JGEixZEpfFXm1P5uXwvEobOFLlHwIWi5v4tjd1ZwfiYmcEilkuD2l+054rvJcYCR1B+uVThHeUbgmNz1FwuxuA==", + "license": "MIT", + "bin": { + "cn": "bin/cn.mjs" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/config": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/config/-/config-5.0.1.tgz", + "integrity": "sha512-vnJGpQA9FjKSTafO9TiNZr1+NiZAIuZXScp034tH+R6VH3Ufe7r+Z730j/f6/KQ2+kNIly6auTYloIaNhpK8lg==", + "license": "MIT", + "dependencies": { + "json5": "^2.2.3" + }, + "engines": { + "node": ">= 20.11.0" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.467", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.467.tgz", + "integrity": "sha512-Nw9X371rtZmYcBSnEijnCDONILjs93biCZO34juC7MBOmK8E+GFnyD4T/WvyrlJkppul7oxg6TwvtKst9SODTw==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/engine.io": { + "version": "6.6.11", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.11.tgz", + "integrity": "sha512-z34mGpLoY/82ZbkX1LqGMIaf3wjVMPL6gVC681QDqnmzJ26synI8qAE32P+a7UazmpJZ8UGrFIeAavgRfXzYrg==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "@types/ws": "^8.5.12", + "accepts": "~1.3.4", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.21.0" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.7.tgz", + "integrity": "sha512-GyAvmPA7gnRCpDQbYACAy79Lg4ewevrmaPzySZz0OVvgyZc3gEfLLqPTYk8wVBScjOEpM9ZO4KryyFpVItjGEA==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.21.0", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/enhanced-resolve": { + "version": "5.26.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.26.0.tgz", + "integrity": "sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-abstract-get": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-abstract-get/-/es-abstract-get-1.0.0.tgz", + "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "is-callable": "^1.2.7", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.4.0.tgz", + "integrity": "sha512-c/A0P0oxkACDc+cKWw8evLXK83oBKgn0qPOqCYT4x9uolpCIJAcYvJC9QYKNDRPsTeGyCrQ326jrvgZWdCdK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.4.tgz", + "integrity": "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-abstract-get": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "is-callable": "^1.2.7", + "is-date-object": "^1.1.0", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-next": { + "version": "16.4.3", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.4.3.tgz", + "integrity": "sha512-j9IqBma6abp800frdUURaizYMgXOK+jNGyq6CsYcM9AxpSJs5D/TgMNGeAmRUilZvA7g9V1Tr71gNg9/SVTuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/eslint-plugin-next": "16.4.3", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^7.1.0", + "globals": "16.4.0", + "typescript-eslint": "^8.56.0" + }, + "peerDependencies": { + "eslint": ">=9.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", + "integrity": "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.16.1", + "resolve": "^2.0.0-next.6" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-typescript/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.14.0.tgz", + "integrity": "sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "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==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", + "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "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/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.2.tgz", + "integrity": "sha512-/kVP2U8z596Ia0gwFrwyIeo74ta5aVmSeU1FfAUMAmcEP5EP7zydmag3SLiN64NaRKef3lxsq7i/g7ltJQDGow==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz", + "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2", + "hasown": "^2.0.4", + "is-callable": "^1.2.7", + "is-document.all": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gaxios": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-8.3.2.tgz", + "integrity": "sha512-7VsaTExxjbDv2ZK1YuC4jbgfN33NI/6/7zdYWUHB6mcg51v4tCBZe+xzjqPzOpVzI+C5k1Kk9OQ80WVrXAHOqw==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/gaxios/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/gaxios/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/gcp-metadata": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-9.2.2.tgz", + "integrity": "sha512-hlaHpCBoNCzoUjMnBZq0LbUiYqPAhXFaAgyNwQqwWU44TZIYJK3RD3o2wkNdvPs8nnUXqJjTudnodLJkLMBjow==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^8.3.0", + "google-logging-utils": "^2.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/geoip-lite": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/geoip-lite/-/geoip-lite-2.0.3.tgz", + "integrity": "sha512-I0v/6sITK5FhEXHmatGSPBMply2nHN/2D743LIx4KkksttZt/E505lDaV6qUwt5RM08MbZW+/4E0KxusLVN39w==", + "license": "Apache-2.0", + "dependencies": { + "chalk": "4.1 - 4.1.2", + "iconv-lite": "0.4.13 - 0.6.3", + "ip-address": "^10.2.0", + "lazy": "1.0.11", + "yauzl": "^3.3.1" + }, + "engines": { + "node": ">=24.0.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.3.tgz", + "integrity": "sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/google-auth-library": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-11.3.1.tgz", + "integrity": "sha512-mQS0R2Ev/DKkzuWSYtDeq15uhDeaWuT2qzLYqJ41LmooxVKHmn5ut3pc6dlHY6wfHW/D7FBmOYjAtkjaD1A7qA==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^8.3.0", + "gcp-metadata": "^9.0.0", + "google-logging-utils": "^2.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/google-logging-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-2.1.0.tgz", + "integrity": "sha512-1R6uiq6XhXW2ZrcGtZpGgVDsYGzSmveVOn8Pp37qe1fbThHZ34e3t3eXj5mVu6PFSVx9fvB1KNVnYgVtq/BRcA==", + "license": "Apache-2.0", + "engines": { + "node": ">=22" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "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/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/intl-tel-input": { + "version": "29.5.7", + "resolved": "https://registry.npmjs.org/intl-tel-input/-/intl-tel-input-29.5.7.tgz", + "integrity": "sha512-KAmo28FkLZMd31KpMQ9aEdUWKiR6FshU1Uj3FTrI3H//LQyJlFK43Wn6frMxKrQq/OSF7u3RatdKPluV+2M8WA==", + "license": "MIT" + }, + "node_modules/ip-address": { + "version": "10.7.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.5.tgz", + "integrity": "sha512-t2v0rkILYv9WXmSL7TpAaU3HXgaFYOPUcH3QHgwQ++gpJuH3bqyMy4Fvt/DJCTKwzH3Cf+bAp5W/mFL39KPLzA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.17.0.tgz", + "integrity": "sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-document.all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", + "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "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==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "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/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/lazy": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/lazy/-/lazy-1.0.11.tgz", + "integrity": "sha512-Y+CjUfLmIpoUCCRl0ub4smrYtGGr5AOa2AKOaWelGHOGz33X/Y/KizefGqbkwfz44+cnq/+9habclf8vOmu2LA==", + "license": "MIT", + "engines": { + "node": ">=0.2.0" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "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==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.63.0.tgz", + "integrity": "sha512-GkBfQG+FUJ41x6y3/lNXiOziVU0Rcqqq0TCzhHS8avsFXHZmOdmfBqsVTvHSaVmbI+wbdGkxHRmhYWhD3N4/yA==", + "license": "ISC", + "peerDependencies": { + "@types/react": "*", + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-postinstall": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.7.tgz", + "integrity": "sha512-jyU4cU4kVAmy/3qjP+DcoQTNXfINHc+dFw54gGqWC8nu0mpV0kOnk7kff+J+Q18SbX0k3Np8Rm7yFjd3phatNQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-patch": "lib/patch.js", + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/next": { + "version": "16.4.3", + "resolved": "https://registry.npmjs.org/next/-/next-16.4.3.tgz", + "integrity": "sha512-OqxKTGi9u80FydydsMNYNjnuje9Au+kvHlt2GZ//ftdpDHm1dxKqAaENoOOTX5wKzn+9vaKUNIfZeyZUuLhCnA==", + "license": "MIT", + "dependencies": { + "@next/env": "16.4.3", + "@swc/helpers": "0.5.23", + "baseline-browser-mapping": "^2.9.19", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.5.23", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.4.3", + "@next/swc-darwin-x64": "16.4.3", + "@next/swc-linux-arm64-gnu": "16.4.3", + "@next/swc-linux-arm64-musl": "16.4.3", + "@next/swc-linux-x64-gnu": "16.4.3", + "@next/swc-linux-x64-musl": "16.4.3", + "@next/swc-win32-arm64-msvc": "16.4.3", + "@next/swc-win32-x64-msvc": "16.4.3", + "sharp": "^0.35.5" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-exports-info": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz", + "integrity": "sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-exports-info/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-releases": { + "version": "2.0.60", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.60.tgz", + "integrity": "sha512-OmO8+GM1JUZqXET720VrJCwkjUCXmCMQm0Z3UDDRbfNLHS4NEAvY60j5jiFv0tkKw2PNNlqOLhahVmwJIOLDIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/notepack.io": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/notepack.io/-/notepack.io-3.0.1.tgz", + "integrity": "sha512-TKC/8zH5pXIAMVQio2TvVDTtPRX+DJPHDqjRbxogtFiByHyzKmy96RA0JtCQJ+WouyyL4A10xomQzgbUT+1jCg==", + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/obug/-/obug-3.0.0.tgz", + "integrity": "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.2.tgz", + "integrity": "sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.64.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.64.1.tgz", + "integrity": "sha512-HCrVhHr5QQuwYoswEUGTb7gGp522yflbv/ST8nF4sFWGuLjQCt17Q/EubpGGSNKYn1tsTCPZxqHhHCfVURu7UA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.64.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright-core": { + "version": "1.64.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.64.1.tgz", + "integrity": "sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qrcode.react": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz", + "integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/qs": { + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.17.0.tgz", + "integrity": "sha512-5LK5BZ2XDHMvcUJaf0CQ5HL3aVgKbpklvByC7qLSxYkfE8COEEbJg5xCHWCTqzqi0Gs4Jn5Mv5v3R38W6edscA==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/radix-ui": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.8.1.tgz", + "integrity": "sha512-HPfpVGizO9xANlsFSauruLhRe/3+iOt3vPX0MXYle/UQi3gorQrSYtBcdiv0BZYewcl2rvL59qrIuLFMGAmUrw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-accessible-icon": "1.2.0", + "@radix-ui/react-accordion": "1.3.1", + "@radix-ui/react-alert-dialog": "1.2.1", + "@radix-ui/react-arrow": "1.1.17", + "@radix-ui/react-aspect-ratio": "1.2.0", + "@radix-ui/react-avatar": "1.3.1", + "@radix-ui/react-checkbox": "1.4.1", + "@radix-ui/react-collapsible": "1.2.1", + "@radix-ui/react-collection": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-context-menu": "2.4.1", + "@radix-ui/react-dialog": "1.3.1", + "@radix-ui/react-direction": "1.2.0", + "@radix-ui/react-dismissable-layer": "1.2.1", + "@radix-ui/react-dropdown-menu": "2.2.1", + "@radix-ui/react-focus-guards": "1.2.0", + "@radix-ui/react-focus-scope": "1.3.1", + "@radix-ui/react-form": "0.3.0", + "@radix-ui/react-hover-card": "1.2.1", + "@radix-ui/react-label": "2.1.17", + "@radix-ui/react-menu": "2.2.1", + "@radix-ui/react-menubar": "1.2.1", + "@radix-ui/react-navigation-menu": "1.4.1", + "@radix-ui/react-one-time-password-field": "0.2.1", + "@radix-ui/react-password-toggle-field": "0.2.1", + "@radix-ui/react-popover": "1.3.1", + "@radix-ui/react-popper": "1.4.0", + "@radix-ui/react-portal": "1.2.0", + "@radix-ui/react-presence": "1.1.11", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-progress": "1.2.1", + "@radix-ui/react-radio-group": "1.5.1", + "@radix-ui/react-roving-focus": "1.2.1", + "@radix-ui/react-scroll-area": "1.4.0", + "@radix-ui/react-select": "2.4.1", + "@radix-ui/react-separator": "1.2.0", + "@radix-ui/react-slider": "1.6.1", + "@radix-ui/react-slot": "1.5.0", + "@radix-ui/react-switch": "1.4.1", + "@radix-ui/react-tabs": "1.2.1", + "@radix-ui/react-toast": "1.3.1", + "@radix-ui/react-toggle": "1.2.1", + "@radix-ui/react-toggle-group": "1.2.1", + "@radix-ui/react-toolbar": "1.2.1", + "@radix-ui/react-tooltip": "1.4.1", + "@radix-ui/react-use-callback-ref": "1.1.5", + "@radix-ui/react-use-controllable-state": "1.3.0", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-escape-keydown": "1.1.6", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-size": "1.1.5", + "@radix-ui/react-visually-hidden": "1.2.13" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/react": { + "version": "19.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.1.tgz", + "integrity": "sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.1.tgz", + "integrity": "sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.1" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/redis": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/redis/-/redis-6.3.0.tgz", + "integrity": "sha512-XFQbPie1lGpKeUZ8ySYY43yLnQy/iIQtHNvIr/1XdaABpb96rUSdm3Mfl+98FEVbv9ZJIzINMno5s0Nh8E5LEw==", + "license": "MIT", + "dependencies": { + "@redis/bloom": "6.3.0", + "@redis/client": "6.3.0", + "@redis/json": "6.3.0", + "@redis/search": "6.3.0", + "@redis/time-series": "6.3.0" + }, + "engines": { + "node": ">= 20.0.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve": { + "version": "2.0.0-next.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", + "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.2", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rolldown": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.15.tgz", + "integrity": "sha512-6odND+fel5zVw3I4Yc2e69gvguDnX0CAAGiNDZSq4ik5WUcaLrkD2GLuog0XcODbor31hZ5ayEMgVDno+KPV5g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@oxc-project/types": "=0.155.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.15", + "@rolldown/binding-android-arm64": "1.2.15", + "@rolldown/binding-darwin-arm64": "1.2.15", + "@rolldown/binding-darwin-x64": "1.2.15", + "@rolldown/binding-freebsd-x64": "1.2.15", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.15", + "@rolldown/binding-linux-arm64-gnu": "1.2.15", + "@rolldown/binding-linux-arm64-musl": "1.2.15", + "@rolldown/binding-linux-ppc64-gnu": "1.2.15", + "@rolldown/binding-linux-s390x-gnu": "1.2.15", + "@rolldown/binding-linux-x64-gnu": "1.2.15", + "@rolldown/binding-linux-x64-musl": "1.2.15", + "@rolldown/binding-openharmony-arm64": "1.2.15", + "@rolldown/binding-win32-arm64-msvc": "1.2.15", + "@rolldown/binding-win32-x64-msvc": "1.2.15" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/server-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz", + "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==", + "license": "MIT" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sharp": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/socket.io": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.4.tgz", + "integrity": "sha512-9ZV9zg8vN8hO3Y25DhR7nUkJ1VDnBhq5O/JrXhFT9s6+F0gqytSASTHVfCFc2mJXaTfARroErplXlzFWwETZqQ==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.8", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.8.tgz", + "integrity": "sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw==", + "license": "MIT", + "dependencies": { + "debug": "~4.4.1", + "ws": "~8.21.0" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-client": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.4.tgz", + "integrity": "sha512-7wjKAvVXYOJsbmUgkWbnhPD4bFpkqrUR0++XOjpGFmqDqsqV1CI93VTraeExLxpjxEqrWgFoSRZu1ihjLWfkAA==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.7.tgz", + "integrity": "sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", + "dev": true, + "license": "MIT" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.1.0.tgz", + "integrity": "sha512-tHNHTxInrYLCga9O9YGxWA3G9/nnzQw8UGAyqGx3Ar1pSTTzIuM4woFSq4SowkXCjJIwq5sIiQvEfRI9tCH1qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "get-intrinsic": "^1.3.0", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.4", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwind-merge": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.7.0.tgz", + "integrity": "sha512-XPPUyAc+cvspz3lHTcR/QgPfW2A0lv/xQNIjX3HGhLR+Nq2lHaLq5MtTesHn8GUr3W3DguT2KT5x3NVgRtYwmA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinybench": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.2.tgz", + "integrity": "sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tw-animate-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", + "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.5.tgz", + "integrity": "sha512-0FHJvLPqZ7KJzp17O13jfsAjsqazgrxBu2zEK95PmUz8lv2+GjRuxUInCr2Rk9Dms3ihN21zJ929ZO43yJ95QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.72.1.tgz", + "integrity": "sha512-3Li9FfPmQp45nxEtFD1ZCrYFI3cYK5kpyNtmm1H7lMagm8u9sy6PTjy3ytwQZs7KNukOyKFHAZ6z6GjYFeaeXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.72.1", + "@typescript-eslint/parser": "8.72.1", + "@typescript-eslint/typescript-estree": "8.72.1", + "@typescript-eslint/utils": "8.72.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/uid2": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-1.0.0.tgz", + "integrity": "sha512-+I6aJUv63YAcY9n4mQreLUt0d4lvwkkopDNmpomkAUz0fAkEMV9pRWxN0EjhW1YfRhcuyHg2v3mwddCDW1+LFQ==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "license": "MIT" + }, + "node_modules/unrs-resolver": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", + "integrity": "sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.4" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.12.2", + "@unrs/resolver-binding-android-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-x64": "1.12.2", + "@unrs/resolver-binding-freebsd-x64": "1.12.2", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-arm64-musl": "1.12.2", + "@unrs/resolver-binding-linux-loong64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-loong64-musl": "1.12.2", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-musl": "1.12.2", + "@unrs/resolver-binding-linux-s390x-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-musl": "1.12.2", + "@unrs/resolver-binding-openharmony-arm64": "1.12.2", + "@unrs/resolver-binding-wasm32-wasi": "1.12.2", + "@unrs/resolver-binding-win32-arm64-msvc": "1.12.2", + "@unrs/resolver-binding-win32-ia32-msvc": "1.12.2", + "@unrs/resolver-binding-win32-x64-msvc": "1.12.2" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz", + "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.5.tgz", + "integrity": "sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.30", + "rolldown": "~1.2.14", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1 || ^0.8.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "peer": true, + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/vite/node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vite/node_modules/postcss": { + "version": "8.5.32", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.32.tgz", + "integrity": "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.20", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/vitest": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.5.tgz", + "integrity": "sha512-cPODOMF8YnkiiYtDdHo6n4wWmBrOBBbOIYH2d33M4l3CdNn72h+qMmO6uXffUN6A5AZFXNND/d+krS9mgkXRfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/mocker": "5.0.5", + "chai": "^6.3.0", + "es-module-lexer": "^2.3.2", + "expect-type": "^1.4.0", + "magic-string": "^1.4.3", + "obug": "^3.0.0", + "picomatch": "^4.0.7", + "std-env": "^4.3.0", + "tinybench": "^6.2.2", + "tinyexec": "^1.3.1", + "tinyglobby": "^0.2.17", + "why-is-node-running": "3.2.1" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^22.12.0 || ^24.0.0 || >=26.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "5.0.5", + "@vitest/browser-preview": "5.0.5", + "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", + "@vitest/coverage-istanbul": "5.0.5", + "@vitest/coverage-v8": "5.0.5", + "@vitest/ui": "5.0.5", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/magic-string": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.5.tgz", + "integrity": "sha512-oLLC2+YvQUfAa+vxoufE/yc1icb87M9iXIRqAoR/2EhE/8cb3SF/+Gi0c2By1B+5Vz2e2vQtUvHiu1+63Znh+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.24", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.24.tgz", + "integrity": "sha512-wk4Mf4pR5mRP7eYuuTBCIQ9d0ud2Fv2jRLQpfgnRjbOxAFHmjKFValgTpitVKzJJS8ajnYQV2Du1SZ8j6b/EUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/why-is-node-running": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.1.tgz", + "integrity": "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==", + "dev": true, + "license": "MIT", + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=20.11" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yauzl": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.4.0.tgz", + "integrity": "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/showcase-next/package.json b/showcase-next/package.json new file mode 100644 index 0000000..095e7c0 --- /dev/null +++ b/showcase-next/package.json @@ -0,0 +1,67 @@ +{ + "name": "ipification-showcase-next", + "version": "0.1.0", + "private": true, + "type": "module", + "engines": { + "node": "24.22.0" + }, + "scripts": { + "dev": "node server.mjs", + "build": "next build", + "start": "NODE_ENV=production node server.mjs", + "lint": "eslint .", + "typecheck": "tsc --noEmit", + "test": "vitest run --exclude 'tests/integration/**'", + "test:integration": "vitest run tests/integration", + "test:e2e": "playwright test", + "config:check": "node scripts/check-config.mjs" + }, + "dependencies": { + "@next/env": "^16.4.3", + "@radix-ui/react-dialog": "^1.3.1", + "@radix-ui/react-select": "^2.4.1", + "@radix-ui/react-slot": "^1.5.0", + "@radix-ui/react-tabs": "^1.2.1", + "@socket.io/redis-adapter": "^8.3.0", + "@socket.io/redis-emitter": "^5.1.0", + "axios": "^1.21.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cn": "^0.5.0", + "config": "^5.0.1", + "geoip-lite": "^2.0.3", + "google-auth-library": "^11.3.1", + "intl-tel-input": "^29.5.7", + "jsonwebtoken": "^9.0.3", + "lucide-react": "^1.63.0", + "next": "^16.4.3", + "qrcode.react": "^4.2.0", + "qs": "^6.17.0", + "radix-ui": "^1.8.1", + "react": "^19.3.1", + "react-dom": "^19.3.1", + "redis": "^6.3.0", + "server-only": "^0.0.1", + "socket.io": "^4.8.4", + "socket.io-client": "^4.8.4", + "tailwind-merge": "^3.7.0" + }, + "devDependencies": { + "@playwright/test": "^1.64.1", + "@tailwindcss/postcss": "^4.3.3", + "@types/config": "^3.3.5", + "@types/geoip-lite": "^1.4.4", + "@types/jsonwebtoken": "^9.0.10", + "@types/node": "^26.6.5", + "@types/qs": "^6.15.1", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "eslint": "^9.39.5", + "eslint-config-next": "^16.4.3", + "tailwindcss": "^4.3.3", + "tw-animate-css": "^1.4.0", + "typescript": "^6.0.3", + "vitest": "^5.0.5" + } +} diff --git a/showcase-next/playwright.config.ts b/showcase-next/playwright.config.ts new file mode 100644 index 0000000..1cbb17e --- /dev/null +++ b/showcase-next/playwright.config.ts @@ -0,0 +1,2 @@ +import {defineConfig,devices} from '@playwright/test'; +export default defineConfig({testDir:'./e2e',fullyParallel:false,workers:1,use:{baseURL:'http://127.0.0.1:3002',trace:'retain-on-failure'},projects:[{name:'chromium',use:{...devices['Desktop Chrome']}}],webServer:{command:'node tests/helpers/e2e-server.mjs',url:'http://127.0.0.1:3002/auth/login',reuseExistingServer:false,timeout:120000}}); diff --git a/showcase-next/postcss.config.mjs b/showcase-next/postcss.config.mjs new file mode 100644 index 0000000..01edca0 --- /dev/null +++ b/showcase-next/postcss.config.mjs @@ -0,0 +1,2 @@ +const config={ plugins:{'@tailwindcss/postcss':{}} }; +export default config; diff --git a/showcase-next/runtime/config.d.mts b/showcase-next/runtime/config.d.mts new file mode 100644 index 0000000..7ea999e --- /dev/null +++ b/showcase-next/runtime/config.d.mts @@ -0,0 +1,6 @@ +import type {ServerConfig} from '../src/server/config/load'; +export function initializeConfigEnvironment(options?:{appRoot?:string;dev?:boolean}):void; +export function loadRuntimeConfig():ServerConfig; +export function validateFeatureEnvironment():void; +export function deepMerge(target:T,source?:Partial):T; +export class ConfigurationError extends Error{} diff --git a/showcase-next/runtime/config.mjs b/showcase-next/runtime/config.mjs new file mode 100644 index 0000000..08421f2 --- /dev/null +++ b/showcase-next/runtime/config.mjs @@ -0,0 +1,40 @@ +import {createRequire} from 'node:module'; +import {readFileSync} from 'node:fs'; +import path from 'node:path'; +import {fileURLToPath} from 'node:url'; +import nextEnv from '@next/env'; +let appRoot=process.env.SHOWCASE_APP_ROOT||path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..'); +const require=createRequire(path.join(appRoot,'package.json')); +let snapshot; +export function initializeConfigEnvironment({appRoot:root=appRoot,dev=process.env.NODE_ENV!=='production'}={}){ + appRoot=root;process.env.SHOWCASE_APP_ROOT=appRoot;nextEnv.loadEnvConfig(appRoot,dev); + process.env.APP_ENV ||= dev?'development':'production'; + process.env.NODE_CONFIG_ENV ||= process.env.APP_ENV; + process.env.NODE_CONFIG_DIR=(process.env.NODE_CONFIG_DIR||path.join(appRoot,'config')).split(path.delimiter).map(p=>path.resolve(appRoot,p)).join(path.delimiter); +} +export function deepMerge(target,source){ + const out={...target};for(const [k,v] of Object.entries(source||{})){ + if(['__proto__','prototype','constructor'].includes(k))continue; + out[k]=v&&typeof v==='object'&&!Array.isArray(v)?deepMerge(out[k]&&typeof out[k]==='object'?out[k]:{},v):v; + }return out; +} +function freeze(value){if(value&&typeof value==='object'){for(const v of Object.values(value))freeze(v);Object.freeze(value);}return value;} +export class ConfigurationError extends Error{constructor(field){super(`INVALID_CONFIG:${field}`);this.name='ConfigurationError';}} +export function validateFeatureEnvironment(){ + if(process.env.PLAY_INTEGRITY_ENABLED==='true')for(const name of ['PLAY_INTEGRITY_PACKAGE_NAME','PLAY_INTEGRITY_USER_FLOW','PLAY_INTEGRITY_STATE_SECRET','PLAY_INTEGRITY_STATE_ISSUER','PLAY_INTEGRITY_STATE_AUDIENCE'])if(!process.env[name])throw new ConfigurationError(name); +} +export function loadRuntimeConfig(){ + if(snapshot)return snapshot; + let raw;try{raw=require('config').util.toObject();}catch{throw new ConfigurationError('json');} + for(const key of ['realm','client_id','client_secret'])if(typeof raw[key]!=='string'||!raw[key])throw new ConfigurationError(key); + for(const key of ['auth_servers','clients'])if(!Array.isArray(raw[key])||!raw[key].length)throw new ConfigurationError(key); + const ids=new Set();for(const server of raw.auth_servers){if(typeof server.id!=='string'||!server.id||ids.has(server.id))throw new ConfigurationError('auth_servers.id');try{new URL(server.url);}catch{throw new ConfigurationError('auth_servers.url');}ids.add(server.id);} + const flows=new Set();for(const c of raw.clients){if(typeof c.user_flow!=='string'||!c.user_flow||flows.has(c.user_flow)||typeof c.scope!=='string'||!c.scope)throw new ConfigurationError('clients');flows.add(c.user_flow);} + if(process.env.NODE_ENV==='production'&&!process.env.APP_ORIGIN)throw new ConfigurationError('APP_ORIGIN'); + if(process.env.APP_ORIGIN){try{const u=new URL(process.env.APP_ORIGIN);if(!['http:','https:'].includes(u.protocol)||u.username||u.password||u.search||u.hash||u.pathname!=='/')throw 0;}catch{throw new ConfigurationError('APP_ORIGIN');}} + validateFeatureEnvironment(); + const locale=JSON.parse(readFileSync(path.join(appRoot,'config/locale.json'),'utf8')); + const appConfig=JSON.parse(readFileSync(path.join(appRoot,'config/app_config.json'),'utf8')); + snapshot=freeze({...raw,clients:raw.clients.map(c=>({...c,client_id:c.client_id||raw.client_id,client_secret:c.client_secret||raw.client_secret})),locale:deepMerge(locale,raw.locale),app_config:deepMerge(appConfig,raw.app_config)}); + return snapshot; +} diff --git a/showcase-next/runtime/socket-server.d.mts b/showcase-next/runtime/socket-server.d.mts new file mode 100644 index 0000000..e7060b6 --- /dev/null +++ b/showcase-next/runtime/socket-server.d.mts @@ -0,0 +1,2 @@ +import type {Server} from 'node:http'; +export function startSocketServer(server:Server,options:{redisUrl:string;adapterKey:string}):Promise<{ready():boolean;close():Promise}>; diff --git a/showcase-next/runtime/socket-server.mjs b/showcase-next/runtime/socket-server.mjs new file mode 100644 index 0000000..d0c653c --- /dev/null +++ b/showcase-next/runtime/socket-server.mjs @@ -0,0 +1,14 @@ +import {Server} from 'socket.io'; +import {createClient} from 'redis'; +import {createAdapter} from '@socket.io/redis-adapter'; +export async function startSocketServer(httpServer,{redisUrl,adapterKey}){ + const pub=createClient({url:redisUrl,socket:{connectTimeout:3000,reconnectStrategy:retries=>Math.min(1000,100+retries*100)}}); + const sub=pub.duplicate();pub.on('error',()=>{});sub.on('error',()=>{}); + await Promise.all([pub.connect(),sub.connect()]); + const io=new Server(httpServer);io.adapter(createAdapter(pub,sub,{key:adapterKey})); + io.on('connection',socket=>{ + socket.emit('messages',{event_name:'connected',socket_id:socket.id}); + socket.on('init',(data,ack)=>{if(!data||typeof data.state!=='string'||!data.state||data.state.length>512)return;socket.join(`auth:${data.state}`);if(typeof ack==='function')ack({joined:true});}); + }); + return {ready:()=>pub.isReady&&sub.isReady,async close(){await new Promise(resolve=>io.close(resolve));await Promise.all([pub.close(),sub.close()]);}}; +} diff --git a/showcase-next/server.mjs b/showcase-next/server.mjs new file mode 100644 index 0000000..414ff46 --- /dev/null +++ b/showcase-next/server.mjs @@ -0,0 +1,18 @@ +import {createServer} from 'node:http'; +import {fileURLToPath} from 'node:url'; +import next from 'next'; +import {startSocketServer} from './runtime/socket-server.mjs'; +import {initializeConfigEnvironment} from './runtime/config.mjs'; +const appRoot=fileURLToPath(new URL('.',import.meta.url)); +const dev=process.env.NODE_ENV!=='production'; +initializeConfigEnvironment({appRoot,dev}); +const port=Number(process.env.PORT||3002); +const app=next({dev,dir:appRoot,hostname:'0.0.0.0',port}); +await app.prepare(); +const handler=app.getRequestHandler(); +let sockets; +const server=createServer((req,res)=>{req.headers['x-showcase-peer-ip']=req.socket.remoteAddress||'127.0.0.1';req.headers['x-showcase-realtime-ready']=sockets?.ready()?'true':'false';res.setHeader('X-Content-Type-Options','nosniff');res.setHeader('Referrer-Policy','no-referrer');res.setHeader('X-Frame-Options','SAMEORIGIN');return handler(req,res);}); +if(process.env.REDIS_URL)sockets=await startSocketServer(server,{redisUrl:process.env.REDIS_URL,adapterKey:(process.env.SHOWCASE_KEY_PREFIX||'showcase-next:development:')+'socket.io'}); +server.listen(port,'0.0.0.0',()=>console.log(`Showcase listening on ${port}`)); +async function close(){const timeout=setTimeout(()=>process.exit(1),9500);timeout.unref();if(sockets)await sockets.close();server.close();server.closeAllConnections();await app.close();process.exit(0);} +process.once('SIGTERM',close);process.once('SIGINT',close); diff --git a/showcase-next/src/app/api/config/route.ts b/showcase-next/src/app/api/config/route.ts new file mode 100644 index 0000000..cfb5229 --- /dev/null +++ b/showcase-next/src/app/api/config/route.ts @@ -0,0 +1,5 @@ +import {resolveContext} from '@/server/http/context'; +import {toPublicConfig} from '@/server/config/public'; +import {json,unavailable} from '@/server/http/response'; +export const dynamic='force-dynamic'; +export function GET(request:Request){try{const c=resolveContext(request);return json(toPublicConfig(c.config,c.origin));}catch{return unavailable();}} diff --git a/showcase-next/src/app/api/geoip/route.ts b/showcase-next/src/app/api/geoip/route.ts new file mode 100644 index 0000000..61904e2 --- /dev/null +++ b/showcase-next/src/app/api/geoip/route.ts @@ -0,0 +1,5 @@ +import geoip from 'geoip-lite'; +import {resolveContext} from '@/server/http/context'; +import {json,unavailable} from '@/server/http/response'; +export const dynamic='force-dynamic'; +export function GET(request:Request){try{const c=resolveContext(request);let country=c.config.default_country_code||'rs';try{country=geoip.lookup(c.clientIp)?.country||country;}catch{}return json({country:country.toLowerCase(),ip:c.clientIp});}catch{return unavailable();}} diff --git a/showcase-next/src/app/auth/login/page.tsx b/showcase-next/src/app/auth/login/page.tsx new file mode 100644 index 0000000..3a23c37 --- /dev/null +++ b/showcase-next/src/app/auth/login/page.tsx @@ -0,0 +1 @@ +export default function Login(){return

IPification Showcase

Mobile identity, made simple.

;} diff --git a/showcase-next/src/app/globals.css b/showcase-next/src/app/globals.css new file mode 100644 index 0000000..a0d9367 --- /dev/null +++ b/showcase-next/src/app/globals.css @@ -0,0 +1,18 @@ +@import "tailwindcss"; +@import "tw-animate-css"; +@theme inline { + --color-background: var(--background); --color-foreground:var(--foreground); + --color-primary:var(--primary); --color-primary-foreground:var(--primary-foreground); + --color-card:var(--card); --color-card-foreground:var(--foreground); + --color-muted:var(--muted); --color-muted-foreground:var(--muted-foreground); + --color-border:var(--border); --color-input:var(--border); --color-ring:var(--primary); + --color-accent:var(--muted); --color-accent-foreground:var(--foreground); + --color-secondary:var(--muted); --color-secondary-foreground:var(--foreground); + --color-destructive:#b42318; --color-popover:#ffffff; --color-popover-foreground:var(--foreground); + --radius-sm:6px; --radius-md:10px; --radius-lg:14px; --radius-xl:20px; +} +:root{--background:#f5f7fa;--foreground:#17283b;--primary:#0064d2;--primary-foreground:#ffffff;--card:#ffffff;--muted:#edf2f7;--muted-foreground:#586b80;--border:#dbe3ec;--radius:0.875rem;} +*{border-color:var(--border)} +body{margin:0;background:var(--background);color:var(--foreground);font-family:Arial,Helvetica,sans-serif;} +button,a,input,select{touch-action:manipulation} +button:not(:disabled),a{cursor:pointer} diff --git a/showcase-next/src/app/layout.tsx b/showcase-next/src/app/layout.tsx new file mode 100644 index 0000000..c998a2f --- /dev/null +++ b/showcase-next/src/app/layout.tsx @@ -0,0 +1,4 @@ +import type {Metadata} from 'next'; +import './globals.css'; +export const metadata:Metadata={title:'IPification Showcase',description:'Explore mobile identity and phone verification'}; +export default function RootLayout({children}:{children:React.ReactNode}){return {children};} diff --git a/showcase-next/src/app/not-found.tsx b/showcase-next/src/app/not-found.tsx new file mode 100644 index 0000000..11380f0 --- /dev/null +++ b/showcase-next/src/app/not-found.tsx @@ -0,0 +1 @@ +export default function NotFound(){return

Page not found

Return to Showcase
;} diff --git a/showcase-next/src/app/route.ts b/showcase-next/src/app/route.ts new file mode 100644 index 0000000..30166da --- /dev/null +++ b/showcase-next/src/app/route.ts @@ -0,0 +1 @@ +export function GET(){return new Response(null,{status:302,headers:{Location:'/auth/login'}});} diff --git a/showcase-next/src/components/ui/alert.tsx b/showcase-next/src/components/ui/alert.tsx new file mode 100644 index 0000000..86310a6 --- /dev/null +++ b/showcase-next/src/components/ui/alert.tsx @@ -0,0 +1,65 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { cn } from "cn" + +const alertVariants = cva( + "relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", + { + variants: { + variant: { + default: "bg-card text-card-foreground", + destructive: + "bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { Alert, AlertTitle, AlertDescription } diff --git a/showcase-next/src/components/ui/button.tsx b/showcase-next/src/components/ui/button.tsx new file mode 100644 index 0000000..e3345d9 --- /dev/null +++ b/showcase-next/src/components/ui/button.tsx @@ -0,0 +1,63 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { cn } from "cn" +import { Slot } from "radix-ui" + +const buttonVariants = cva( + "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3", + sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3", + "icon-sm": "size-8", + "icon-lg": "size-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant = "default", + size = "default", + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot.Root : "button" + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/showcase-next/src/components/ui/card.tsx b/showcase-next/src/components/ui/card.tsx new file mode 100644 index 0000000..6aebd4c --- /dev/null +++ b/showcase-next/src/components/ui/card.tsx @@ -0,0 +1,91 @@ +import * as React from "react" +import { cn } from "cn" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/showcase-next/src/components/ui/dialog.tsx b/showcase-next/src/components/ui/dialog.tsx new file mode 100644 index 0000000..c754f39 --- /dev/null +++ b/showcase-next/src/components/ui/dialog.tsx @@ -0,0 +1,158 @@ +"use client" + +import * as React from "react" +import { cn } from "cn" +import { XIcon } from "lucide-react" +import { Dialog as DialogPrimitive } from "radix-ui" + +import { Button } from "@/components/ui/button" + +function Dialog({ + ...props +}: React.ComponentProps) { + return +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogContent({ + className, + children, + showCloseButton = true, + ...props +}: React.ComponentProps & { + showCloseButton?: boolean +}) { + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogFooter({ + className, + showCloseButton = false, + children, + ...props +}: React.ComponentProps<"div"> & { + showCloseButton?: boolean +}) { + return ( +
+ {children} + {showCloseButton && ( + + + + )} +
+ ) +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/showcase-next/src/components/ui/input.tsx b/showcase-next/src/components/ui/input.tsx new file mode 100644 index 0000000..ddb9b31 --- /dev/null +++ b/showcase-next/src/components/ui/input.tsx @@ -0,0 +1,20 @@ +import * as React from "react" +import { cn } from "cn" + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + + ) +} + +export { Input } diff --git a/showcase-next/src/components/ui/label.tsx b/showcase-next/src/components/ui/label.tsx new file mode 100644 index 0000000..5aff746 --- /dev/null +++ b/showcase-next/src/components/ui/label.tsx @@ -0,0 +1,23 @@ +"use client" + +import * as React from "react" +import { cn } from "cn" +import { Label as LabelPrimitive } from "radix-ui" + +function Label({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Label } diff --git a/showcase-next/src/components/ui/select.tsx b/showcase-next/src/components/ui/select.tsx new file mode 100644 index 0000000..8058e26 --- /dev/null +++ b/showcase-next/src/components/ui/select.tsx @@ -0,0 +1,189 @@ +"use client" + +import * as React from "react" +import { cn } from "cn" +import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react" +import { Select as SelectPrimitive } from "radix-ui" + +function Select({ + ...props +}: React.ComponentProps) { + return +} + +function SelectGroup({ + ...props +}: React.ComponentProps) { + return +} + +function SelectValue({ + ...props +}: React.ComponentProps) { + return +} + +function SelectTrigger({ + className, + size = "default", + children, + ...props +}: React.ComponentProps & { + size?: "sm" | "default" +}) { + return ( + + {children} + + + + + ) +} + +function SelectContent({ + className, + children, + position = "item-aligned", + align = "center", + ...props +}: React.ComponentProps) { + return ( + + + + + {children} + + + + + ) +} + +function SelectLabel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SelectItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function SelectSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SelectScrollUpButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function SelectScrollDownButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +} diff --git a/showcase-next/src/components/ui/tabs.tsx b/showcase-next/src/components/ui/tabs.tsx new file mode 100644 index 0000000..6e17474 --- /dev/null +++ b/showcase-next/src/components/ui/tabs.tsx @@ -0,0 +1,90 @@ +"use client" + +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { cn } from "cn" +import { Tabs as TabsPrimitive } from "radix-ui" + +function Tabs({ + className, + orientation = "horizontal", + ...props +}: React.ComponentProps) { + return ( + + ) +} + +const tabsListVariants = cva( + "group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-[orientation=horizontal]/tabs:h-9 group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none", + { + variants: { + variant: { + default: "bg-muted", + line: "gap-1 bg-transparent", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function TabsList({ + className, + variant = "default", + ...props +}: React.ComponentProps & + VariantProps) { + return ( + + ) +} + +function TabsTrigger({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function TabsContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants } diff --git a/showcase-next/src/lib/utils.ts b/showcase-next/src/lib/utils.ts new file mode 100644 index 0000000..8728d13 --- /dev/null +++ b/showcase-next/src/lib/utils.ts @@ -0,0 +1,3 @@ +import {clsx,type ClassValue} from 'clsx'; +import {twMerge} from 'tailwind-merge'; +export function cn(...inputs:ClassValue[]){return twMerge(clsx(inputs));} diff --git a/showcase-next/src/server/config/load.ts b/showcase-next/src/server/config/load.ts new file mode 100644 index 0000000..0420cc2 --- /dev/null +++ b/showcase-next/src/server/config/load.ts @@ -0,0 +1,6 @@ +import 'server-only'; +import {loadRuntimeConfig} from '../../../runtime/config.mjs'; +import type {AuthServer,PublicClient} from '@/shared/contracts'; +export interface ServerClient extends PublicClient{client_secret:string} +export interface ServerConfig{realm:string;client_id:string;client_secret:string;auth_servers:AuthServer[];clients:ServerClient[];locale:Record;app_config:Record;title?:string;app_logo_url?:string;app_logo_style?:string;favicon_url?:string;default_country_code?:string;disabled_select_country?:boolean;play_integrity_verify_domains?:string[]} +export const getConfig=():ServerConfig=>loadRuntimeConfig(); diff --git a/showcase-next/src/server/config/public.ts b/showcase-next/src/server/config/public.ts new file mode 100644 index 0000000..5532e13 --- /dev/null +++ b/showcase-next/src/server/config/public.ts @@ -0,0 +1,12 @@ +import 'server-only'; +import type {ServerConfig} from './load'; +import type {PublicConfig,PublicShowcaseConfig,PublicClient} from '@/shared/contracts'; +import pkg from '../../../package.json'; +export function toPublicConfig(c:ServerConfig,origin:string):PublicConfig{ + const clients=c.clients.map(client=>{ + const result:PublicClient={user_flow:client.user_flow,client_id:client.client_id,scope:client.scope,redirect_uri:`${origin}/auth/callback/${client.user_flow}`}; + for(const field of ['title','channel','auth_server_url'] as const)if(client[field]!==undefined)result[field]=client[field];return result; + }); + return {version:pkg.version,auth_servers:c.auth_servers.map(({id,url})=>({id,url})),realm:c.realm,clients,app_config:c.app_config}; +} +export function toPublicShowcaseConfig(c:ServerConfig,origin:string):PublicShowcaseConfig{return {...toPublicConfig(c,origin),locale:c.locale,title:c.title||'IPification Showcase',appEnv:process.env.APP_ENV||'development',defaultCountry:c.default_country_code,disabledSelectCountry:!!c.disabled_select_country,logoUrl:c.app_logo_url,logoStyle:c.app_logo_style,faviconUrl:c.favicon_url||'/images/logo.ico',primaryColor:c.app_config.branding?.primary_color};} diff --git a/showcase-next/src/server/http/body.ts b/showcase-next/src/server/http/body.ts new file mode 100644 index 0000000..faad196 --- /dev/null +++ b/showcase-next/src/server/http/body.ts @@ -0,0 +1,12 @@ +export async function readBody(request:Request,limitBytes=102400):Promise>{ + const contentType=request.headers.get('content-type')?.split(';')[0].trim(); + if(contentType!=='application/json'&&contentType!=='application/x-www-form-urlencoded')return {}; + const reader=request.body?.getReader();if(!reader)return {}; + const parts:Uint8Array[]=[];let size=0; + try{while(true){const {done,value}=await reader.read();if(done)break;size+=value.length;if(size>limitBytes){await reader.cancel();throw new Error('INVALID_REQUEST');}parts.push(value);} + const text=Buffer.concat(parts).toString('utf8');if(!text)return {}; + if(contentType==='application/json'){const value=JSON.parse(text);if(!value||typeof value!=='object')throw 0;return value;} + const result:Record=Object.create(null);for(const [k,v] of new URLSearchParams(text)){if(['__proto__','constructor','prototype'].includes(k))continue;result[k]=result[k]===undefined?v:Array.isArray(result[k])?[...result[k],v]:[result[k],v];}return result; + }catch{throw new Error('INVALID_REQUEST');}finally{reader.releaseLock();} +} +export function query(request:Request):Record{const params=new URL(request.url).searchParams;const result:Record=Object.create(null);for(const key of new Set(params.keys())){const values=params.getAll(key);result[key]=values.length===1?values[0]:values;}return result;} diff --git a/showcase-next/src/server/http/context.ts b/showcase-next/src/server/http/context.ts new file mode 100644 index 0000000..ca8ff7b --- /dev/null +++ b/showcase-next/src/server/http/context.ts @@ -0,0 +1,12 @@ +import {getConfig} from '@/server/config/load'; +export function resolveRequestIdentity(request:Request,options:{origin?:string;trustProxyHops:number}){ + const url=new URL(request.url);const trusted=options.trustProxyHops>0; + const host=trusted?request.headers.get('x-forwarded-host')?.split(',')[0].trim()||request.headers.get('host')||url.host:request.headers.get('host')||url.host; + const protocol=trusted?request.headers.get('x-forwarded-proto')?.split(',')[0].trim()||url.protocol.slice(0,-1):url.protocol.slice(0,-1); + let incoming:URL;try{if(!['http','https'].includes(protocol))throw 0;incoming=new URL(`${protocol}://${host}`);if(incoming.username||incoming.password||incoming.pathname!=='/')throw 0;}catch{throw new Error('INVALID_HOST');} + const chain=request.headers.get('x-forwarded-for')?.split(',').map(x=>x.trim())||[]; + return {origin:options.origin?.replace(/\/$/,'')||incoming.origin,hostname:incoming.hostname.toLowerCase(),clientIp:trusted?chain[Math.max(0,chain.length-options.trustProxyHops)]||'127.0.0.1':request.headers.get('x-showcase-peer-ip')||'127.0.0.1'}; +} +export function resolveContext(request:Request){return {...resolveRequestIdentity(request,{origin:process.env.APP_ORIGIN,trustProxyHops:Number(process.env.TRUST_PROXY_HOPS||0)}),config:getConfig(),appEnv:process.env.APP_ENV||'development'};} +export type RequestContext=ReturnType; +export function findServer(context:RequestContext,id:unknown){return id?context.config.auth_servers.find(s=>s.id===id):context.config.auth_servers[0];} diff --git a/showcase-next/src/server/http/headers.ts b/showcase-next/src/server/http/headers.ts new file mode 100644 index 0000000..7c6d38b --- /dev/null +++ b/showcase-next/src/server/http/headers.ts @@ -0,0 +1 @@ +export const securityHeaders={'X-Content-Type-Options':'nosniff','Referrer-Policy':'no-referrer','X-Frame-Options':'SAMEORIGIN'}; diff --git a/showcase-next/src/server/http/response.ts b/showcase-next/src/server/http/response.ts new file mode 100644 index 0000000..5979248 --- /dev/null +++ b/showcase-next/src/server/http/response.ts @@ -0,0 +1,5 @@ +export const privateHeaders={'Cache-Control':'private, no-store'}; +export function json(body:unknown,status=200,headers:HeadersInit={}){return Response.json(body,{status,headers:{...privateHeaders,...headers}});} +export function text(body:string,status=200){return new Response(body,{status,headers:{...privateHeaders,'Content-Type':'text/html; charset=utf-8'}});} +export function legacyRedirect(location:string){return new Response(null,{status:302,headers:{...privateHeaders,Location:location}});} +export function unavailable(){return json({error:'SERVICE_UNAVAILABLE'},503);} diff --git a/showcase-next/src/server/realtime/notifier.ts b/showcase-next/src/server/realtime/notifier.ts new file mode 100644 index 0000000..c2be88f --- /dev/null +++ b/showcase-next/src/server/realtime/notifier.ts @@ -0,0 +1,4 @@ +import 'server-only'; +import {Emitter} from '@socket.io/redis-emitter'; +import {getRedis,key} from '@/server/store/redis'; +export async function notifyAuthComplete(state:string,url:string){const redis=await getRedis();if(!redis.isReady)throw new Error('REALTIME_UNAVAILABLE');new Emitter(redis,{key:key('socket.io')}).to(`auth:${state}`).emit('messages',{event_name:'url',url});} diff --git a/showcase-next/src/server/session/cookies.ts b/showcase-next/src/server/session/cookies.ts new file mode 100644 index 0000000..84bcbea --- /dev/null +++ b/showcase-next/src/server/session/cookies.ts @@ -0,0 +1,3 @@ +const cookieName='showcase_next_sid'; +export function sessionId(request:Request){const cookie=request.headers.get('cookie')?.split(';').map(x=>x.trim()).find(x=>x.startsWith(cookieName+'='));const id=cookie?.slice(cookieName.length+1)||'';return /^[a-f0-9]{64}$/.test(id)?id:'';} +export function setSessionCookie(response:Response,id:string,origin:string){response.headers.append('Set-Cookie',`${cookieName}=${id}; HttpOnly; SameSite=Lax; Path=/; Max-Age=3600${origin.startsWith('https:')?'; Secure':''}`);return response;} diff --git a/showcase-next/src/server/session/store.ts b/showcase-next/src/server/session/store.ts new file mode 100644 index 0000000..4423e48 --- /dev/null +++ b/showcase-next/src/server/session/store.ts @@ -0,0 +1,8 @@ +import {randomBytes} from 'node:crypto'; +import {getRedis,key} from '@/server/store/redis'; +import type {SessionData} from '@/shared/contracts'; +const valid=(id:string)=>/^[a-f0-9]{64}$/.test(id); +export async function createSession(data:SessionData,previousId?:string){const id=randomBytes(32).toString('hex');await(await getRedis()).set(key('session:'+id),JSON.stringify(data),{EX:3600});if(previousId&&valid(previousId))await deleteSession(previousId);return id;} +export async function readSession(id:string):Promise{if(!valid(id))return null;const raw=await(await getRedis()).get(key('session:'+id));if(!raw)return null;try{return JSON.parse(raw);}catch{return null;}} +export async function deleteSession(id:string){if(valid(id))await(await getRedis()).del(key('session:'+id));} +export async function consumeSessionError(id:string):Promise{if(!valid(id))return null;return await(await getRedis()).eval(`local v=redis.call('GET',KEYS[1]);if not v then return false end;local data=cjson.decode(v);local e=data.errorMessage;if not e then return false end;data.errorMessage=nil;local ttl=redis.call('PTTL',KEYS[1]);if ttl>0 then redis.call('SET',KEYS[1],cjson.encode(data),'PX',ttl) end;return e`,{keys:[key('session:'+id)],arguments:[]}) as string|null;} diff --git a/showcase-next/src/server/shared/logger.ts b/showcase-next/src/server/shared/logger.ts new file mode 100644 index 0000000..90a1908 --- /dev/null +++ b/showcase-next/src/server/shared/logger.ts @@ -0,0 +1 @@ +export function logRequest(fields:{requestId?:string;method?:string;path?:string;status?:number;duration?:number;endpoint?:string;decision?:string;reasonCodes?:string[]}){console.info(JSON.stringify(fields));} diff --git a/showcase-next/src/server/store/auth-results.ts b/showcase-next/src/server/store/auth-results.ts new file mode 100644 index 0000000..822caa4 --- /dev/null +++ b/showcase-next/src/server/store/auth-results.ts @@ -0,0 +1,12 @@ +import {getRedis,key} from './redis'; +import type {DisplayResult,UserInfo} from '@/shared/contracts'; +type Store={get:(key:string)=>Promise;set:(key:string,value:any)=>Promise}; +export function createAuthResultStore(store:Store,delay:(ms:number)=>Promise=ms=>new Promise(r=>setTimeout(r,ms))){ + return { + async saveResult(state:string,result:DisplayResult,raw:UserInfo){await store.set(state,result);await store.set(`${state}-user-info`,raw);}, + async waitForResult(state?:string):Promise{for(let i=0;i<=4;i++){const value=await store.get(state||'');if(value)return value;await delay(1000);}return undefined;}, + async getUserInfo(state?:string):Promise{return (await store.get(`${state}-user-info`))||undefined;} + }; +} +const store=createAuthResultStore({async get(id){const value=await(await getRedis()).get(key('result:'+id));return value?JSON.parse(value):null;},async set(id,value){await(await getRedis()).set(key('result:'+id),JSON.stringify(value),{PX:3600000});}}); +export const {saveResult,waitForResult,getUserInfo}=store; diff --git a/showcase-next/src/server/store/rate-limit.ts b/showcase-next/src/server/store/rate-limit.ts new file mode 100644 index 0000000..faf1d8a --- /dev/null +++ b/showcase-next/src/server/store/rate-limit.ts @@ -0,0 +1,14 @@ +import {createHash} from 'node:crypto'; +import {getRedis,key} from './redis'; +const positive=(name:string,fallback:number)=>{const n=parseInt(process.env[name]||'',10);return n>0?n:fallback;}; +export async function checkTs43Limit(context:{clientIp:string},body:Record){ + const windowMs=positive('TS43_RATE_LIMIT_WINDOW_MS',60000); + const identity=createHash('sha256').update(String(body.login_hint||'')).digest('hex'); + const stages=[{id:'ip:'+context.clientIp,limit:positive('TS43_RATE_LIMIT_IP_MAX',20)},{id:`identity:${body.client_id||''}:${identity}`,limit:positive('TS43_RATE_LIMIT_IDENTITY_MAX',5)}]; + let headers:Record={}; + for(const stage of stages){ + const [count,ttl]=await(await getRedis()).eval("local n=redis.call('INCR',KEYS[1]);if n==1 then redis.call('PEXPIRE',KEYS[1],ARGV[1]) end;return {n,redis.call('PTTL',KEYS[1])}",{keys:[key('rate:'+stage.id)],arguments:[String(windowMs)]}) as number[]; + const reset=Math.max(1,Math.ceil(ttl/1000));headers={'RateLimit-Policy':`${stage.limit};w=${Math.ceil(windowMs/1000)}`,'RateLimit':`limit=${stage.limit}, remaining=${Math.max(0,stage.limit-count)}, reset=${reset}`}; + if(count>stage.limit)return {allowed:false,headers:{...headers,'Retry-After':String(reset)}}; + }return {allowed:true,headers}; +} diff --git a/showcase-next/src/server/store/redis.ts b/showcase-next/src/server/store/redis.ts new file mode 100644 index 0000000..103e0c8 --- /dev/null +++ b/showcase-next/src/server/store/redis.ts @@ -0,0 +1,13 @@ +import 'server-only'; +import {createClient} from 'redis'; +function newClient(url:string){return createClient({url,socket:{connectTimeout:3000,reconnectStrategy:retries=>retries>5?new Error('REDIS_UNAVAILABLE'):Math.min(1000,100+retries*100)},disableOfflineQueue:true});} +type Client=ReturnType; +const clients=new Map>(); +export function key(suffix:string){return (process.env.SHOWCASE_KEY_PREFIX||'showcase-next:development:')+suffix;} +export async function getRedis():Promise{ + const url=process.env.REDIS_URL;if(!url)throw new Error('REDIS_UNAVAILABLE'); + const existing=clients.get(url);if(existing)return existing; + const pending=(async()=>{const client=newClient(url);client.on('error',()=>{});await client.connect();return client;})(); + clients.set(url,pending);pending.catch(()=>clients.delete(url));return pending; +} +export async function closeRedis(){for(const p of clients.values()){try{(await p).destroy();}catch{}}clients.clear();} diff --git a/showcase-next/src/shared/contracts.ts b/showcase-next/src/shared/contracts.ts new file mode 100644 index 0000000..ce2578c --- /dev/null +++ b/showcase-next/src/shared/contracts.ts @@ -0,0 +1,7 @@ +export type UserInfo=Record; +export interface AuthServer {id:string;url:string} +export interface PublicClient {user_flow:string;client_id:string;scope:string;title?:string;channel?:string;redirect_uri?:string;auth_server_url?:string} +export interface PublicConfig {version:string;auth_servers:AuthServer[];realm:string;clients:PublicClient[];app_config:Record} +export interface PublicShowcaseConfig extends PublicConfig {locale:Record;title:string;appEnv:string;defaultCountry?:string;disabledSelectCountry:boolean;logoUrl?:string;logoStyle?:string;faviconUrl:string;primaryColor?:string} +export interface DisplayResult{userInfo:UserInfo;client_id:string;client_title:string;state:string} +export interface SessionData{isAuthenticated:boolean;userData?:DisplayResult;errorMessage?:string} diff --git a/showcase-next/tests/contracts/bootstrap.test.ts b/showcase-next/tests/contracts/bootstrap.test.ts new file mode 100644 index 0000000..82eb731 --- /dev/null +++ b/showcase-next/tests/contracts/bootstrap.test.ts @@ -0,0 +1,9 @@ +import { expect,test } from 'vitest'; +import { startApp } from '../helpers/processes'; +test('custom server serves Next pages, legacy root redirect and not-found',async()=>{ + const app=await startApp();try { + const root=await fetch(app.url,{redirect:'manual'});expect(root.status).toBe(302);expect(root.headers.get('location')).toBe('/auth/login'); + const login=await fetch(app.url+'/auth/login');expect(login.status).toBe(200);expect(login.headers.get('content-type')).toContain('text/html'); + expect((await fetch(app.url+'/missing-page')).status).toBe(404); + }finally{await app.stop();} +}); diff --git a/showcase-next/tests/contracts/config-geoip.test.ts b/showcase-next/tests/contracts/config-geoip.test.ts new file mode 100644 index 0000000..d653a8b --- /dev/null +++ b/showcase-next/tests/contracts/config-geoip.test.ts @@ -0,0 +1,12 @@ +import {test,expect} from 'vitest'; +import {startApp} from '../helpers/processes'; +import {configFixture} from '../helpers/config'; +test('mounted configuration is redacted and geoip retains fallback country',async()=>{ + const config=configFixture({default_country_code:'vn'}); + const app=await startApp({NODE_CONFIG_DIR:config.dir,APP_ORIGIN:'https://canonical.example'}); + try{ + const r=await fetch(app.url+'/api/config');expect(r.status,r.status===200?'':await r.text()+app.output()).toBe(200);const data=await r.json(); + expect(data.clients[0].client_id).toBe('fixture-client');expect(data.clients[0].redirect_uri).toBe('https://canonical.example/auth/callback/pvn_ip');expect(JSON.stringify(data)).not.toContain('fixture-secret'); + const geo=await (await fetch(app.url+'/api/geoip')).json();expect(geo.country).toBe('vn'); + }finally{await app.stop();config.remove();} +}); diff --git a/showcase-next/tests/helpers/config.ts b/showcase-next/tests/helpers/config.ts new file mode 100644 index 0000000..8f5bd26 --- /dev/null +++ b/showcase-next/tests/helpers/config.ts @@ -0,0 +1,8 @@ +import {mkdtempSync,writeFileSync,rmSync} from 'node:fs'; +import {tmpdir} from 'node:os'; +import path from 'node:path'; +export function configFixture(overrides:Record={}){ + const dir=mkdtempSync(path.join(tmpdir(),'showcase-fixture-')); + const data={realm:'demo',client_id:'fixture-client',client_secret:'fixture-secret',auth_servers:[{id:'stage',url:'http://127.0.0.1:19999'}],clients:[{user_flow:'pvn_ip',scope:'openid ip:phone_verify',title:'IP Phone Number Verification'},{user_flow:'login_ip',scope:'openid ip:phone',title:'IP Login'},{user_flow:'pvn_sms',scope:'openid',title:'SMS'}],...overrides}; + writeFileSync(path.join(dir,'default.json'),JSON.stringify(data));return {dir,data,remove:()=>rmSync(dir,{recursive:true,force:true})}; +} diff --git a/showcase-next/tests/helpers/e2e-server.mjs b/showcase-next/tests/helpers/e2e-server.mjs new file mode 100644 index 0000000..15ec9de --- /dev/null +++ b/showcase-next/tests/helpers/e2e-server.mjs @@ -0,0 +1,4 @@ +import {spawn} from 'node:child_process'; +const child=spawn(process.execPath,['server.mjs'],{env:{...process.env,NODE_ENV:'production'},stdio:'inherit'}); +for(const signal of ['SIGTERM','SIGINT'])process.once(signal,()=>child.kill(signal)); +child.once('exit',code=>process.exit(code??0)); diff --git a/showcase-next/tests/helpers/processes.ts b/showcase-next/tests/helpers/processes.ts new file mode 100644 index 0000000..f6a94ce --- /dev/null +++ b/showcase-next/tests/helpers/processes.ts @@ -0,0 +1,27 @@ +import { spawn, type ChildProcess } from 'node:child_process'; +import { createServer } from 'node:net'; +export async function freePort(): Promise { + const server=createServer(); await new Promise(r=>server.listen(0,'127.0.0.1',r)); + const port=(server.address() as {port:number}).port; + await new Promise(r=>server.close(()=>r())); return port; +} +export async function startApp(env: Record={}) { + const port=env.PORT?Number(env.PORT):await freePort(); let output=''; + const child=spawn(process.execPath,['server.mjs'],{env:{...process.env,NODE_ENV:'development',PORT:String(port),...env},stdio:['ignore','pipe','pipe']}); + child.stdout?.on('data',d=>output+=d);child.stderr?.on('data',d=>output+=d); + const url=`http://127.0.0.1:${port}`; + try { + for(let i=0;i<300;i++) { + if(child.exitCode!==null) throw new Error(output); + try { const response=await fetch(url+'/auth/login'); if(response.status<500) return {child,url,stop:()=>stop(child),output:()=>output}; } catch {} + await new Promise(r=>setTimeout(r,100)); + } + throw new Error('Server did not become ready: '+output); + } catch(error) { await stop(child);throw error; } +} +export async function stop(child: ChildProcess) { + if(child.exitCode!==null || child.signalCode!==null)return; + const exited=new Promise(r=>child.once('exit',()=>r()));child.kill('SIGTERM'); + const timer=setTimeout(()=>child.kill('SIGKILL'),8000); + await exited;clearTimeout(timer); +} diff --git a/showcase-next/tests/helpers/redis.ts b/showcase-next/tests/helpers/redis.ts new file mode 100644 index 0000000..2930815 --- /dev/null +++ b/showcase-next/tests/helpers/redis.ts @@ -0,0 +1,11 @@ +import {spawn} from 'node:child_process'; +import {createClient} from 'redis'; +import {freePort,stop} from './processes'; +export async function startRedis(){ + const port=await freePort();const child=spawn('redis-server',['--bind','127.0.0.1','--port',String(port),'--save','','--appendonly','no'],{stdio:'ignore'}); + const url=`redis://127.0.0.1:${port}`; + const client=createClient({url,socket:{reconnectStrategy:false}});client.on('error',()=>{}); + try{for(let i=0;i<50;i++){try{await client.connect();break;}catch{await new Promise(r=>setTimeout(r,50));}}await client.ping();} + catch(error){await stop(child);throw error;} + return {url,client,stop:async()=>{client.destroy();await stop(child);}}; +} diff --git a/showcase-next/tests/helpers/server-only.ts b/showcase-next/tests/helpers/server-only.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/showcase-next/tests/helpers/server-only.ts @@ -0,0 +1 @@ +export {}; diff --git a/showcase-next/tests/integration/auth-results.test.ts b/showcase-next/tests/integration/auth-results.test.ts new file mode 100644 index 0000000..a48e8e0 --- /dev/null +++ b/showcase-next/tests/integration/auth-results.test.ts @@ -0,0 +1,11 @@ +import {test,expect} from 'vitest'; +import {startRedis} from '../helpers/redis'; +import {saveResult,waitForResult,getUserInfo} from '@/server/store/auth-results'; +import {closeRedis} from '@/server/store/redis'; +test('results are shared, retain one hour TTL and S2S reads do not consume',async()=>{ + const redis=await startRedis();process.env.REDIS_URL=redis.url;process.env.SHOWCASE_KEY_PREFIX='result-test:'; + try{const value={userInfo:{sub:'fixture'},state:'s',client_id:'c',client_title:'Phone'};await saveResult('s',value,{sub:'fixture'}); + expect(JSON.parse((await redis.client.get('result-test:result:s'))!)).toEqual(value);expect(await waitForResult('s')).toEqual(value); + expect(await getUserInfo('s')).toEqual({sub:'fixture'});expect(await getUserInfo('s')).toEqual({sub:'fixture'});expect(await redis.client.pTTL('result-test:result:s')).toBeGreaterThan(3590000); + }finally{await closeRedis();await redis.stop();} +}); diff --git a/showcase-next/tests/integration/rate-limit.test.ts b/showcase-next/tests/integration/rate-limit.test.ts new file mode 100644 index 0000000..cd47df6 --- /dev/null +++ b/showcase-next/tests/integration/rate-limit.test.ts @@ -0,0 +1,11 @@ +import {test,expect} from 'vitest'; +import {startRedis} from '../helpers/redis'; +import {checkTs43Limit} from '@/server/store/rate-limit'; +import {closeRedis} from '@/server/store/redis'; +test('identity limit has shared atomic count and Retry-After',async()=>{ + const redis=await startRedis();process.env.REDIS_URL=redis.url;process.env.SHOWCASE_KEY_PREFIX='limit-test:'; + try{ + const results=await Promise.all(Array.from({length:6},()=>checkTs43Limit({clientIp:'203.0.113.1'},{client_id:'c',login_hint:'phone'}))); + expect(results.filter(r=>r.allowed)).toHaveLength(5);expect(results.find(r=>!r.allowed)?.headers['Retry-After']).toBeDefined(); + }finally{await closeRedis();await redis.stop();} +}); diff --git a/showcase-next/tests/integration/realtime.test.ts b/showcase-next/tests/integration/realtime.test.ts new file mode 100644 index 0000000..975750b --- /dev/null +++ b/showcase-next/tests/integration/realtime.test.ts @@ -0,0 +1,20 @@ +import {test,expect} from 'vitest'; +import {createServer} from 'node:http'; +import {io} from 'socket.io-client'; +import {startRedis} from '../helpers/redis'; +import {startSocketServer} from '../../runtime/socket-server.mjs'; +import {notifyAuthComplete} from '@/server/realtime/notifier'; +import {closeRedis} from '@/server/store/redis'; +test.each(['websocket','polling'])('real %s Socket.IO clients receive matching cross-server room events',async transport=>{ + const redis=await startRedis();process.env.REDIS_URL=redis.url;process.env.SHOWCASE_KEY_PREFIX='realtime-test:'; + const a=createServer(),b=createServer(); + const runtimeA=await startSocketServer(a,{redisUrl:redis.url,adapterKey:'realtime-test:socket.io'});const runtimeB=await startSocketServer(b,{redisUrl:redis.url,adapterKey:'realtime-test:socket.io'}); + await Promise.all([new Promise(r=>a.listen(0,'127.0.0.1',r)),new Promise(r=>b.listen(0,'127.0.0.1',r))]); + const client=io(`http://127.0.0.1:${(a.address() as any).port}`,{transports:[transport]}); + try{ + await new Promise(r=>client.on('connect',()=>r()));await new Promise(r=>client.emit('init',{state:'test-qrcode'},()=>r())); + const message=new Promise(r=>client.on('messages',data=>{if(data.event_name==='url')r(data);})); + await notifyAuthComplete('test-qrcode','http://showcase.test/auth/complete?state=test-qrcode'); + expect(await message).toEqual({event_name:'url',url:'http://showcase.test/auth/complete?state=test-qrcode'}); + }finally{client.disconnect();await runtimeA.close();await runtimeB.close();a.closeAllConnections();b.closeAllConnections();await closeRedis();await redis.stop();} +}); diff --git a/showcase-next/tests/integration/sessions.test.ts b/showcase-next/tests/integration/sessions.test.ts new file mode 100644 index 0000000..31f07fb --- /dev/null +++ b/showcase-next/tests/integration/sessions.test.ts @@ -0,0 +1,14 @@ +import {test,expect} from 'vitest'; +import {startRedis} from '../helpers/redis'; +import {createSession,readSession,consumeSessionError,deleteSession} from '@/server/session/store'; +import {closeRedis} from '@/server/store/redis'; +test('opaque sessions rotate, stay server-side and atomically consume errors',async()=>{ + const redis=await startRedis();process.env.REDIS_URL=redis.url;process.env.SHOWCASE_KEY_PREFIX='session-test:'; + try{ + const id=await createSession({isAuthenticated:false,errorMessage:'once'});expect(id).toMatch(/^[a-f0-9]{64}$/); + const errors=await Promise.all([consumeSessionError(id),consumeSessionError(id)]);expect(errors.filter(Boolean)).toEqual(['once']); + const next=await createSession({isAuthenticated:true,userData:{userInfo:{sub:'u'},state:'s',client_id:'c',client_title:'Phone'}},id); + expect(await readSession(id)).toBeNull();expect((await readSession(next))?.isAuthenticated).toBe(true);expect(await redis.client.ttl('session-test:session:'+next)).toBeGreaterThan(3590); + await deleteSession(next);expect(await readSession(next)).toBeNull();expect(await readSession('invalid')).toBeNull(); + }finally{await closeRedis();await redis.stop();} +}); diff --git a/showcase-next/tests/unit/auth-results.test.ts b/showcase-next/tests/unit/auth-results.test.ts new file mode 100644 index 0000000..74694fa --- /dev/null +++ b/showcase-next/tests/unit/auth-results.test.ts @@ -0,0 +1,8 @@ +import {test,expect} from 'vitest'; +import {createAuthResultStore} from '@/server/store/auth-results'; +test('result writes remain sequential and total miss retains five reads and sleeps',async()=>{ + const operations:string[]=[]; + const store=createAuthResultStore({get:async()=>{operations.push('read');return null;},set:async(key)=>{operations.push(key);}},async()=>{operations.push('sleep');}); + await store.saveResult('s',{userInfo:{},state:'s',client_id:'c',client_title:'Phone'},{});expect(operations).toEqual(['s','s-user-info']);operations.length=0; + expect(await store.waitForResult()).toBeUndefined();expect(operations).toEqual(Array.from({length:5},()=>['read','sleep']).flat()); +}); diff --git a/showcase-next/tests/unit/config.test.ts b/showcase-next/tests/unit/config.test.ts new file mode 100644 index 0000000..f652486 --- /dev/null +++ b/showcase-next/tests/unit/config.test.ts @@ -0,0 +1,17 @@ +import {test,expect} from 'vitest'; +import {mkdtempSync,writeFileSync,rmSync} from 'node:fs'; +import {tmpdir} from 'node:os'; +import path from 'node:path'; +import {spawnSync} from 'node:child_process'; +const fixture={realm:'demo',client_id:'root',client_secret:'fixture-secret',auth_servers:[{id:'stage',url:'https://provider.invalid'}],clients:[{user_flow:'pvn_ip',scope:'openid',client_id:'',client_secret:''}],locale:{tabs:{pnv:'Custom'}}}; +test('runtime config loads mounted JSON, stage overlays and packaged defaults independently of cwd',()=>{ + const dir=mkdtempSync(path.join(tmpdir(),'showcase-config-')); + try{ + writeFileSync(path.join(dir,'default.json'),JSON.stringify(fixture)); + writeFileSync(path.join(dir,'stage.json'),JSON.stringify({clients:[{user_flow:'login_ip',scope:'openid'}]})); + const entry=path.resolve('runtime/config.mjs'); + const script=`import {initializeConfigEnvironment,loadRuntimeConfig} from ${JSON.stringify('file://'+entry)};initializeConfigEnvironment({appRoot:${JSON.stringify(process.cwd())},dev:false});let c=loadRuntimeConfig();console.log(JSON.stringify({flow:c.clients[0].user_flow,id:c.clients[0].client_id,count:c.clients.length,pnv:c.locale.tabs.pnv,login:c.locale.tabs.login,version:c.app_config.config_version,frozen:Object.isFrozen(c)}));`; + const result=spawnSync(process.execPath,['--input-type=module','-e',script],{cwd:tmpdir(),env:{...process.env,NODE_CONFIG_DIR:dir,NODE_CONFIG_ENV:'stage',APP_ENV:'stage',NODE_ENV:'production',APP_ORIGIN:'https://showcase.example'},encoding:'utf8'}); + expect(result.status,result.stderr).toBe(0);expect(JSON.parse(result.stdout.trim())).toEqual({flow:'login_ip',id:'root',count:1,pnv:'Custom',login:'Login',version:'1.0.0',frozen:true}); + }finally{rmSync(dir,{recursive:true,force:true});} +}); diff --git a/showcase-next/tests/unit/context.test.ts b/showcase-next/tests/unit/context.test.ts new file mode 100644 index 0000000..556a784 --- /dev/null +++ b/showcase-next/tests/unit/context.test.ts @@ -0,0 +1,11 @@ +import {expect,test} from 'vitest'; +import {resolveRequestIdentity} from '@/server/http/context'; +import {readBody} from '@/server/http/body'; +test('canonical origin differs from trusted incoming verification hostname',()=>{ + expect(resolveRequestIdentity(new Request('http://internal:3002/',{headers:{'x-forwarded-host':'Verify.Example:443','x-forwarded-proto':'https','x-forwarded-for':'203.0.113.7'}}),{origin:'https://canonical.example',trustProxyHops:1})).toEqual({origin:'https://canonical.example',hostname:'verify.example',clientIp:'203.0.113.7'}); + expect(resolveRequestIdentity(new Request('http://localhost/',{headers:{'x-forwarded-host':'attacker.invalid','x-forwarded-for':'spoof'}}),{trustProxyHops:0}).hostname).toBe('localhost'); +}); +test('body reader accepts forms and rejects streamed oversize without trusting content length',async()=>{ + expect(await readBody(new Request('http://test',{method:'POST',headers:{'content-type':'application/x-www-form-urlencoded'},body:'state=a&state=b'}),1024)).toEqual({state:['a','b']}); + await expect(readBody(new Request('http://test',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({x:'a'.repeat(16384)})}),16384)).rejects.toThrow('INVALID_REQUEST'); +}); diff --git a/showcase-next/tsconfig.json b/showcase-next/tsconfig.json new file mode 100644 index 0000000..de7a240 --- /dev/null +++ b/showcase-next/tsconfig.json @@ -0,0 +1 @@ +{"compilerOptions":{"target":"ES2022","lib":["dom","dom.iterable","esnext"],"allowJs":true,"skipLibCheck":true,"strict":true,"noEmit":true,"esModuleInterop":true,"module":"esnext","moduleResolution":"bundler","resolveJsonModule":true,"isolatedModules":true,"jsx":"react-jsx","incremental":true,"plugins":[{"name":"next"}],"paths":{"@/*":["./src/*"]}},"include":["next-env.d.ts","**/*.ts","**/*.tsx",".next/types/**/*.ts",".next/dev/types/**/*.ts"],"exclude":["node_modules"]} diff --git a/showcase-next/vitest.config.ts b/showcase-next/vitest.config.ts new file mode 100644 index 0000000..f62c0e4 --- /dev/null +++ b/showcase-next/vitest.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vitest/config'; +import { fileURLToPath } from 'node:url'; +export default defineConfig({ + resolve:{alias:{'@':fileURLToPath(new URL('./src',import.meta.url)), 'server-only':fileURLToPath(new URL('./tests/helpers/server-only.ts',import.meta.url))}}, + test:{include:['tests/**/*.test.ts'],testTimeout:30000,hookTimeout:60000,fileParallelism:false}, +}); From e92eeb41a20b26c70ef2cd7d2e7a817d6b17470b Mon Sep 17 00:00:00 2001 From: Manh Le Date: Wed, 28 Oct 2026 21:42:28 +0700 Subject: [PATCH 03/13] feat: migrate auth APIs and React login QR flows --- showcase-next/.gitignore | 3 +- showcase-next/config/locale.json | 50 ++++++++ showcase-next/e2e/login.spec.ts | 5 + showcase-next/e2e/phone.spec.ts | 5 + showcase-next/e2e/qr.spec.ts | 6 + showcase-next/next-env.d.ts | 4 +- .../public/images/btn-active-next-blue.svg | 4 + .../public/images/btn-active-next-gray.svg | 4 + .../public/images/btn-active-next.svg | 1 + showcase-next/public/images/check.svg | 5 + showcase-next/public/images/close_btn.svg | 1 + showcase-next/public/images/error.svg | 6 + showcase-next/public/images/info.svg | 1 + showcase-next/public/images/logo.ico | Bin 0 -> 101743 bytes showcase-next/public/images/logo/vil.svg | 36 ++++++ .../app/api/play-integrity/attempt/route.ts | 3 + .../play-integrity/token-exchange/route.ts | 3 + .../app/api/play-integrity/verify/route.ts | 3 + .../callback/[userFlow]/[serverId]/route.ts | 3 + showcase-next/src/app/auth/complete/route.ts | 2 + .../src/app/auth/login/error/route.ts | 5 + showcase-next/src/app/auth/login/page.tsx | 13 +- .../src/app/auth/mobile/login/route.ts | 1 + .../src/app/auth/qrcode/complete/page.tsx | 1 + .../src/app/auth/qrcode/error/page.tsx | 1 + .../src/app/auth/s2s/signin/route.ts | 4 + showcase-next/src/app/auth/start/route.ts | 2 + showcase-next/src/app/globals.css | 3 + showcase-next/src/app/sms/auth/route.ts | 1 + showcase-next/src/app/sms/log/route.ts | 1 + showcase-next/src/app/sms/token/route.ts | 1 + showcase-next/src/app/ts43/auth/route.ts | 1 + showcase-next/src/app/ts43/log/route.ts | 1 + showcase-next/src/app/ts43/token/route.ts | 1 + showcase-next/src/app/user/info/page.tsx | 6 + .../src/features/login/info-dialogs.tsx | 3 + .../src/features/login/login-error.tsx | 4 + .../src/features/login/server-select.tsx | 4 + showcase-next/src/features/login/showcase.tsx | 37 ++++++ .../src/features/login/use-preferences.tsx | 14 +++ .../src/features/phone/phone-input.tsx | 22 ++++ showcase-next/src/features/qr/qr-dialog.tsx | 9 ++ .../src/features/qr/use-auth-socket.tsx | 15 +++ .../src/features/results/result-view.tsx | 4 + showcase-next/src/server/auth/mobile.ts | 10 ++ showcase-next/src/server/auth/provider.ts | 11 ++ showcase-next/src/server/auth/web.ts | 34 ++++++ showcase-next/src/server/ciba/handlers.ts | 22 ++++ showcase-next/src/server/ciba/provider.ts | 19 +++ showcase-next/src/server/ciba/sms.ts | 3 + showcase-next/src/server/ciba/ts43.ts | 3 + showcase-next/src/server/http/response.ts | 2 +- .../src/server/integrity/attempts.ts | 35 ++++++ .../src/server/integrity/handlers.ts | 38 ++++++ .../src/server/integrity/verifier.ts | 113 ++++++++++++++++++ .../src/server/shared/canonical-json.ts | 67 +++++++++++ .../src/server/shared/http-client.ts | 4 + .../src/server/shared/phone-number.ts | 13 ++ showcase-next/src/server/store/integrity.ts | 7 ++ showcase-next/src/shared/flow-catalog.ts | 4 + showcase-next/tests/contracts/ciba.test.ts | 18 +++ .../tests/contracts/integrity.test.ts | 16 +++ .../tests/contracts/mobile-s2s.test.ts | 10 ++ .../tests/contracts/web-auth.test.ts | 20 ++++ showcase-next/tests/helpers/e2e-server.mjs | 28 ++++- showcase-next/tests/helpers/provider.ts | 13 ++ .../integration/integrity-concurrency.test.ts | 15 +++ .../tests/unit/canonical-json.test.ts | 3 + .../tests/unit/integrity-verifier.test.ts | 6 + 69 files changed, 805 insertions(+), 8 deletions(-) create mode 100644 showcase-next/config/locale.json create mode 100644 showcase-next/e2e/login.spec.ts create mode 100644 showcase-next/e2e/phone.spec.ts create mode 100644 showcase-next/e2e/qr.spec.ts create mode 100644 showcase-next/public/images/btn-active-next-blue.svg create mode 100644 showcase-next/public/images/btn-active-next-gray.svg create mode 100644 showcase-next/public/images/btn-active-next.svg create mode 100644 showcase-next/public/images/check.svg create mode 100644 showcase-next/public/images/close_btn.svg create mode 100644 showcase-next/public/images/error.svg create mode 100644 showcase-next/public/images/info.svg create mode 100644 showcase-next/public/images/logo.ico create mode 100644 showcase-next/public/images/logo/vil.svg create mode 100644 showcase-next/src/app/api/play-integrity/attempt/route.ts create mode 100644 showcase-next/src/app/api/play-integrity/token-exchange/route.ts create mode 100644 showcase-next/src/app/api/play-integrity/verify/route.ts create mode 100644 showcase-next/src/app/auth/callback/[userFlow]/[serverId]/route.ts create mode 100644 showcase-next/src/app/auth/complete/route.ts create mode 100644 showcase-next/src/app/auth/login/error/route.ts create mode 100644 showcase-next/src/app/auth/mobile/login/route.ts create mode 100644 showcase-next/src/app/auth/qrcode/complete/page.tsx create mode 100644 showcase-next/src/app/auth/qrcode/error/page.tsx create mode 100644 showcase-next/src/app/auth/s2s/signin/route.ts create mode 100644 showcase-next/src/app/auth/start/route.ts create mode 100644 showcase-next/src/app/sms/auth/route.ts create mode 100644 showcase-next/src/app/sms/log/route.ts create mode 100644 showcase-next/src/app/sms/token/route.ts create mode 100644 showcase-next/src/app/ts43/auth/route.ts create mode 100644 showcase-next/src/app/ts43/log/route.ts create mode 100644 showcase-next/src/app/ts43/token/route.ts create mode 100644 showcase-next/src/app/user/info/page.tsx create mode 100644 showcase-next/src/features/login/info-dialogs.tsx create mode 100644 showcase-next/src/features/login/login-error.tsx create mode 100644 showcase-next/src/features/login/server-select.tsx create mode 100644 showcase-next/src/features/login/showcase.tsx create mode 100644 showcase-next/src/features/login/use-preferences.tsx create mode 100644 showcase-next/src/features/phone/phone-input.tsx create mode 100644 showcase-next/src/features/qr/qr-dialog.tsx create mode 100644 showcase-next/src/features/qr/use-auth-socket.tsx create mode 100644 showcase-next/src/features/results/result-view.tsx create mode 100644 showcase-next/src/server/auth/mobile.ts create mode 100644 showcase-next/src/server/auth/provider.ts create mode 100644 showcase-next/src/server/auth/web.ts create mode 100644 showcase-next/src/server/ciba/handlers.ts create mode 100644 showcase-next/src/server/ciba/provider.ts create mode 100644 showcase-next/src/server/ciba/sms.ts create mode 100644 showcase-next/src/server/ciba/ts43.ts create mode 100644 showcase-next/src/server/integrity/attempts.ts create mode 100644 showcase-next/src/server/integrity/handlers.ts create mode 100644 showcase-next/src/server/integrity/verifier.ts create mode 100644 showcase-next/src/server/shared/canonical-json.ts create mode 100644 showcase-next/src/server/shared/http-client.ts create mode 100644 showcase-next/src/server/shared/phone-number.ts create mode 100644 showcase-next/src/server/store/integrity.ts create mode 100644 showcase-next/src/shared/flow-catalog.ts create mode 100644 showcase-next/tests/contracts/ciba.test.ts create mode 100644 showcase-next/tests/contracts/integrity.test.ts create mode 100644 showcase-next/tests/contracts/mobile-s2s.test.ts create mode 100644 showcase-next/tests/contracts/web-auth.test.ts create mode 100644 showcase-next/tests/helpers/provider.ts create mode 100644 showcase-next/tests/integration/integrity-concurrency.test.ts create mode 100644 showcase-next/tests/unit/canonical-json.test.ts create mode 100644 showcase-next/tests/unit/integrity-verifier.test.ts diff --git a/showcase-next/.gitignore b/showcase-next/.gitignore index b6466c1..f5ff4cc 100644 --- a/showcase-next/.gitignore +++ b/showcase-next/.gitignore @@ -3,7 +3,8 @@ node_modules/ .env* !.env.example config/default.json -config/local*.json +config/local.json +config/local-*.json config/stage.json config/production.json *.tsbuildinfo diff --git a/showcase-next/config/locale.json b/showcase-next/config/locale.json new file mode 100644 index 0000000..3ee0637 --- /dev/null +++ b/showcase-next/config/locale.json @@ -0,0 +1,50 @@ +{ + "app": { + "name": "IPification features showcase" + }, + "tabs": { + "pnv": "PNV", + "login": "Login", + "identity": "Identity" + }, + "sections": { + "pnv": { + "title": "Phone Number Verification", + "description": "Verify phone number through IP or IM auth." + }, + "login": { + "title": "Quick Access", + "description": "Login through IP or IM auth with phone number sharing." + }, + "identity": { + "title": "Mobile ID & Know Your Customer", + "description": "Identify user with anonymous identity (mobile_id) or obtain user info with KYC." + } + }, + "buttons": { + "ip": "IP", + "ip_plus": "IP+", + "im": "IM", + "sim": "SIM", + "ip_via_app": "IP via App", + "anonymous_identity": "Anonymous Identity", + "kyc": "KYC", + "sms": "SMS" + }, + "footer": { + "viettel_legal": "Please be kindly aware that by operating to initiate and/or access user accounts, you have agreed on entire content set out in the Privacy terms and Conditions of the Service; and Terms of Use by", + "viettel": "Viettel", + "privacy_policy": "Privacy Policy", + "about": "About", + "help": "Help" + }, + "modals": { + "about_title": "About this demo", + "server_label": "server", + "state_label": "state", + "developer_link": "Developer.ipification", + "phone_required": "Please enter phone number", + "phone_invalid": "Your phone is invalid" + } +} + diff --git a/showcase-next/e2e/login.spec.ts b/showcase-next/e2e/login.spec.ts new file mode 100644 index 0000000..2d0be03 --- /dev/null +++ b/showcase-next/e2e/login.spec.ts @@ -0,0 +1,5 @@ +import {test,expect} from '@playwright/test'; +test('configured flow tabs and preferences survive refresh without Bootstrap',async({page})=>{ + await page.goto('/auth/login');await expect(page.getByRole('tab',{name:'PNV',exact:true})).toBeVisible();await page.getByRole('tab',{name:'Login',exact:true}).click();await expect(page).toHaveURL(/#login$/);await page.reload();await expect(page.getByRole('tab',{name:'Login',exact:true})).toHaveAttribute('aria-selected','true'); + expect(await page.locator('script[src*="jquery"],link[href*="bootstrap"]').count()).toBe(0); +}); diff --git a/showcase-next/e2e/phone.spec.ts b/showcase-next/e2e/phone.spec.ts new file mode 100644 index 0000000..e054ae7 --- /dev/null +++ b/showcase-next/e2e/phone.spec.ts @@ -0,0 +1,5 @@ +import {test,expect} from '@playwright/test'; +test('phone verification validates input and works at mobile width',async({page})=>{ + await page.setViewportSize({width:375,height:812});await page.goto('/auth/login#pnv');const phone=page.getByLabel('Phone number',{exact:true});await expect(phone).toBeVisible();await phone.fill('2025550123');await expect(page.getByRole('button',{name:'IP',exact:true})).toBeEnabled(); + expect(await page.evaluate(()=>document.documentElement.scrollWidth<=window.innerWidth)).toBe(true); +}); diff --git a/showcase-next/e2e/qr.spec.ts b/showcase-next/e2e/qr.spec.ts new file mode 100644 index 0000000..9adc7ba --- /dev/null +++ b/showcase-next/e2e/qr.spec.ts @@ -0,0 +1,6 @@ +import {test,expect} from '@playwright/test'; +test('desktop QR receives callback from independent phone browser session',async({page,browser})=>{ + await page.goto('/auth/login#login');await page.getByRole('button',{name:'IP',exact:true}).click();await expect(page.getByRole('dialog')).toBeVisible(); + const link=page.getByTestId('qr-auth-link');const url=await link.getAttribute('href');expect(url).toContain('-qrcode'); + const phone=await browser.newContext();try{const phonePage=await phone.newPage();await phonePage.goto(url!);await expect(phonePage.getByRole('heading',{name:'Success',exact:true})).toBeVisible();await expect(page).toHaveURL(/\/user\/info$/);await expect(page.locator('pre')).toContainText('browser-user');}finally{await phone.close();} +}); diff --git a/showcase-next/next-env.d.ts b/showcase-next/next-env.d.ts index a419cbe..ce4e94a 100644 --- a/showcase-next/next-env.d.ts +++ b/showcase-next/next-env.d.ts @@ -1,7 +1,7 @@ /// /// -import "./.next/dev/types/routes.d.ts"; -import "./.next/dev/types/root-params.d.ts"; +import "./.next/types/routes.d.ts"; +import "./.next/types/root-params.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/showcase-next/public/images/btn-active-next-blue.svg b/showcase-next/public/images/btn-active-next-blue.svg new file mode 100644 index 0000000..aa6ca9e --- /dev/null +++ b/showcase-next/public/images/btn-active-next-blue.svg @@ -0,0 +1,4 @@ + + + + diff --git a/showcase-next/public/images/btn-active-next-gray.svg b/showcase-next/public/images/btn-active-next-gray.svg new file mode 100644 index 0000000..07b2a9d --- /dev/null +++ b/showcase-next/public/images/btn-active-next-gray.svg @@ -0,0 +1,4 @@ + + + + diff --git a/showcase-next/public/images/btn-active-next.svg b/showcase-next/public/images/btn-active-next.svg new file mode 100644 index 0000000..886ba88 --- /dev/null +++ b/showcase-next/public/images/btn-active-next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/showcase-next/public/images/check.svg b/showcase-next/public/images/check.svg new file mode 100644 index 0000000..114ebd7 --- /dev/null +++ b/showcase-next/public/images/check.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/showcase-next/public/images/close_btn.svg b/showcase-next/public/images/close_btn.svg new file mode 100644 index 0000000..26b72c7 --- /dev/null +++ b/showcase-next/public/images/close_btn.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/showcase-next/public/images/error.svg b/showcase-next/public/images/error.svg new file mode 100644 index 0000000..90caa21 --- /dev/null +++ b/showcase-next/public/images/error.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/showcase-next/public/images/info.svg b/showcase-next/public/images/info.svg new file mode 100644 index 0000000..ceb3ff4 --- /dev/null +++ b/showcase-next/public/images/info.svg @@ -0,0 +1 @@ +info \ No newline at end of file diff --git a/showcase-next/public/images/logo.ico b/showcase-next/public/images/logo.ico new file mode 100644 index 0000000000000000000000000000000000000000..4b06fa14b3ca7be521e10e31b13117336a894399 GIT binary patch literal 101743 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 literal 0 HcmV?d00001 diff --git a/showcase-next/public/images/logo/vil.svg b/showcase-next/public/images/logo/vil.svg new file mode 100644 index 0000000..6b9f2d4 --- /dev/null +++ b/showcase-next/public/images/logo/vil.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/showcase-next/src/app/api/play-integrity/attempt/route.ts b/showcase-next/src/app/api/play-integrity/attempt/route.ts new file mode 100644 index 0000000..8c18c23 --- /dev/null +++ b/showcase-next/src/app/api/play-integrity/attempt/route.ts @@ -0,0 +1,3 @@ +import {handleIntegrity} from '@/server/integrity/handlers'; +export const runtime='nodejs'; +export async function POST(request:Request){return handleIntegrity(request,'attempt');} diff --git a/showcase-next/src/app/api/play-integrity/token-exchange/route.ts b/showcase-next/src/app/api/play-integrity/token-exchange/route.ts new file mode 100644 index 0000000..877b8e4 --- /dev/null +++ b/showcase-next/src/app/api/play-integrity/token-exchange/route.ts @@ -0,0 +1,3 @@ +import {handleIntegrity} from '@/server/integrity/handlers'; +export const runtime='nodejs'; +export async function POST(request:Request){return handleIntegrity(request,'token-exchange');} diff --git a/showcase-next/src/app/api/play-integrity/verify/route.ts b/showcase-next/src/app/api/play-integrity/verify/route.ts new file mode 100644 index 0000000..9c8eb1f --- /dev/null +++ b/showcase-next/src/app/api/play-integrity/verify/route.ts @@ -0,0 +1,3 @@ +import {handleIntegrity} from '@/server/integrity/handlers'; +export const runtime='nodejs'; +export async function POST(request:Request){return handleIntegrity(request,'verify');} diff --git a/showcase-next/src/app/auth/callback/[userFlow]/[serverId]/route.ts b/showcase-next/src/app/auth/callback/[userFlow]/[serverId]/route.ts new file mode 100644 index 0000000..6b8ab89 --- /dev/null +++ b/showcase-next/src/app/auth/callback/[userFlow]/[serverId]/route.ts @@ -0,0 +1,3 @@ +import {callback} from '@/server/auth/web'; +export const dynamic='force-dynamic'; +export async function GET(request:Request,context:{params:Promise<{userFlow:string;serverId:string}>}){return callback(request,await context.params);} diff --git a/showcase-next/src/app/auth/complete/route.ts b/showcase-next/src/app/auth/complete/route.ts new file mode 100644 index 0000000..a8a3191 --- /dev/null +++ b/showcase-next/src/app/auth/complete/route.ts @@ -0,0 +1,2 @@ +export {complete as GET} from '@/server/auth/web'; +export const dynamic='force-dynamic'; diff --git a/showcase-next/src/app/auth/login/error/route.ts b/showcase-next/src/app/auth/login/error/route.ts new file mode 100644 index 0000000..9fe70ed --- /dev/null +++ b/showcase-next/src/app/auth/login/error/route.ts @@ -0,0 +1,5 @@ +import {consumeSessionError} from '@/server/session/store'; +import {sessionId} from '@/server/session/cookies'; +import {json,unavailable} from '@/server/http/response'; +export const dynamic='force-dynamic'; +export async function GET(request:Request){try{return json({error_message:await consumeSessionError(sessionId(request))});}catch{return unavailable();}} diff --git a/showcase-next/src/app/auth/login/page.tsx b/showcase-next/src/app/auth/login/page.tsx index 3a23c37..0e63914 100644 --- a/showcase-next/src/app/auth/login/page.tsx +++ b/showcase-next/src/app/auth/login/page.tsx @@ -1 +1,12 @@ -export default function Login(){return

IPification Showcase

Mobile identity, made simple.

;} +import {headers} from 'next/headers'; +import {getConfig} from '@/server/config/load'; +import {toPublicShowcaseConfig} from '@/server/config/public'; +import {resolveRequestIdentity} from '@/server/http/context'; +import {Showcase} from '@/features/login/showcase'; +export const dynamic='force-dynamic'; +export default async function Login(){ + let config;try{const h=await headers();const identity=resolveRequestIdentity(new Request('http://localhost/auth/login',{headers:h}),{origin:process.env.APP_ORIGIN,trustProxyHops:Number(process.env.TRUST_PROXY_HOPS||0)});config=toPublicShowcaseConfig(getConfig(),identity.origin);} + catch{} + if(!config)return

Showcase is not ready

Check runtime configuration and service readiness, then retry.

; + return ; +} diff --git a/showcase-next/src/app/auth/mobile/login/route.ts b/showcase-next/src/app/auth/mobile/login/route.ts new file mode 100644 index 0000000..2d6b7e1 --- /dev/null +++ b/showcase-next/src/app/auth/mobile/login/route.ts @@ -0,0 +1 @@ +export {mobileLogin as POST} from '@/server/auth/mobile'; diff --git a/showcase-next/src/app/auth/qrcode/complete/page.tsx b/showcase-next/src/app/auth/qrcode/complete/page.tsx new file mode 100644 index 0000000..abd8f2d --- /dev/null +++ b/showcase-next/src/app/auth/qrcode/complete/page.tsx @@ -0,0 +1 @@ +export default function Success(){return
✓

Success

You've been authenticated. Continue on your desktop.

;} diff --git a/showcase-next/src/app/auth/qrcode/error/page.tsx b/showcase-next/src/app/auth/qrcode/error/page.tsx new file mode 100644 index 0000000..3ced9db --- /dev/null +++ b/showcase-next/src/app/auth/qrcode/error/page.tsx @@ -0,0 +1 @@ +export default function Failure(){return

Failure

Authentication unsuccessful

Try again
;} diff --git a/showcase-next/src/app/auth/s2s/signin/route.ts b/showcase-next/src/app/auth/s2s/signin/route.ts new file mode 100644 index 0000000..a03b099 --- /dev/null +++ b/showcase-next/src/app/auth/s2s/signin/route.ts @@ -0,0 +1,4 @@ +import {readBody} from '@/server/http/body'; +import {getUserInfo} from '@/server/store/auth-results'; +import {json,unavailable} from '@/server/http/response'; +export async function POST(request:Request){try{const body=await readBody(request);const user=await getUserInfo(body.state);return user?json(user):new Response(null,{status:401});}catch{return unavailable();}} diff --git a/showcase-next/src/app/auth/start/route.ts b/showcase-next/src/app/auth/start/route.ts new file mode 100644 index 0000000..8bd92d5 --- /dev/null +++ b/showcase-next/src/app/auth/start/route.ts @@ -0,0 +1,2 @@ +export {start as GET} from '@/server/auth/web'; +export const dynamic='force-dynamic'; diff --git a/showcase-next/src/app/globals.css b/showcase-next/src/app/globals.css index a0d9367..33e1dee 100644 --- a/showcase-next/src/app/globals.css +++ b/showcase-next/src/app/globals.css @@ -1,5 +1,6 @@ @import "tailwindcss"; @import "tw-animate-css"; +@import "intl-tel-input/styles"; @theme inline { --color-background: var(--background); --color-foreground:var(--foreground); --color-primary:var(--primary); --color-primary-foreground:var(--primary-foreground); @@ -16,3 +17,5 @@ body{margin:0;background:var(--background);color:var(--foreground);font-family:Arial,Helvetica,sans-serif;} button,a,input,select{touch-action:manipulation} button:not(:disabled),a{cursor:pointer} +.iti{width:100%;--iti-border-color:var(--border);--iti-dropdown-bg:white;} +.iti input{width:100%} diff --git a/showcase-next/src/app/sms/auth/route.ts b/showcase-next/src/app/sms/auth/route.ts new file mode 100644 index 0000000..2dc07a1 --- /dev/null +++ b/showcase-next/src/app/sms/auth/route.ts @@ -0,0 +1 @@ +export {smsAuth as POST} from '@/server/ciba/handlers'; diff --git a/showcase-next/src/app/sms/log/route.ts b/showcase-next/src/app/sms/log/route.ts new file mode 100644 index 0000000..7229a6f --- /dev/null +++ b/showcase-next/src/app/sms/log/route.ts @@ -0,0 +1 @@ +export {diagnostics as POST} from '@/server/ciba/handlers'; diff --git a/showcase-next/src/app/sms/token/route.ts b/showcase-next/src/app/sms/token/route.ts new file mode 100644 index 0000000..c24f610 --- /dev/null +++ b/showcase-next/src/app/sms/token/route.ts @@ -0,0 +1 @@ +export {smsToken as POST} from '@/server/ciba/handlers'; diff --git a/showcase-next/src/app/ts43/auth/route.ts b/showcase-next/src/app/ts43/auth/route.ts new file mode 100644 index 0000000..a523bed --- /dev/null +++ b/showcase-next/src/app/ts43/auth/route.ts @@ -0,0 +1 @@ +export {ts43Auth as POST} from '@/server/ciba/handlers'; diff --git a/showcase-next/src/app/ts43/log/route.ts b/showcase-next/src/app/ts43/log/route.ts new file mode 100644 index 0000000..7229a6f --- /dev/null +++ b/showcase-next/src/app/ts43/log/route.ts @@ -0,0 +1 @@ +export {diagnostics as POST} from '@/server/ciba/handlers'; diff --git a/showcase-next/src/app/ts43/token/route.ts b/showcase-next/src/app/ts43/token/route.ts new file mode 100644 index 0000000..f911caa --- /dev/null +++ b/showcase-next/src/app/ts43/token/route.ts @@ -0,0 +1 @@ +export {ts43Token as POST} from '@/server/ciba/handlers'; diff --git a/showcase-next/src/app/user/info/page.tsx b/showcase-next/src/app/user/info/page.tsx new file mode 100644 index 0000000..e771ae7 --- /dev/null +++ b/showcase-next/src/app/user/info/page.tsx @@ -0,0 +1,6 @@ +import {cookies} from 'next/headers'; +import {redirect} from 'next/navigation'; +import {readSession} from '@/server/session/store'; +import {ResultView} from '@/features/results/result-view'; +export const dynamic='force-dynamic'; +export default async function Info(){const id=(await cookies()).get('showcase_next_sid')?.value||'';const session=await readSession(id);if(!session?.isAuthenticated||!session.userData)redirect('/');return ;} diff --git a/showcase-next/src/features/login/info-dialogs.tsx b/showcase-next/src/features/login/info-dialogs.tsx new file mode 100644 index 0000000..2c97a85 --- /dev/null +++ b/showcase-next/src/features/login/info-dialogs.tsx @@ -0,0 +1,3 @@ +'use client'; +import {Dialog,DialogContent,DialogHeader,DialogTitle,DialogDescription,DialogTrigger} from '@/components/ui/dialog'; +export function InfoDialogs({server,locale}:{server?:{id:string;url:string};locale:Record}){return {locale.footer.about}{locale.modals.about_title}{locale.modals.server_label}: {server?.id}

{server?.url}

{locale.modals.developer_link}
;} diff --git a/showcase-next/src/features/login/login-error.tsx b/showcase-next/src/features/login/login-error.tsx new file mode 100644 index 0000000..7f2132d --- /dev/null +++ b/showcase-next/src/features/login/login-error.tsx @@ -0,0 +1,4 @@ +'use client'; +import {useEffect,useRef,useState} from 'react'; +import {Alert,AlertDescription} from '@/components/ui/alert'; +export function LoginError(){const [message,setMessage]=useState(''),started=useRef(false);useEffect(()=>{if(started.current)return;started.current=true;fetch('/auth/login/error',{cache:'no-store'}).then(r=>r.json()).then(data=>{if(data.error_message)setMessage(data.error_message);}).catch(()=>{});},[]);return message?{message}:null;} diff --git a/showcase-next/src/features/login/server-select.tsx b/showcase-next/src/features/login/server-select.tsx new file mode 100644 index 0000000..118af20 --- /dev/null +++ b/showcase-next/src/features/login/server-select.tsx @@ -0,0 +1,4 @@ +'use client'; +import {Select,SelectTrigger,SelectValue,SelectContent,SelectItem} from '@/components/ui/select'; +import type {AuthServer} from '@/shared/contracts'; +export function ServerSelect({servers,value,onChange}:{servers:AuthServer[];value:string;onChange:(value:string)=>void}){if(servers.length<2)return null;return ;} diff --git a/showcase-next/src/features/login/showcase.tsx b/showcase-next/src/features/login/showcase.tsx new file mode 100644 index 0000000..cf0f6d4 --- /dev/null +++ b/showcase-next/src/features/login/showcase.tsx @@ -0,0 +1,37 @@ +'use client'; +import Image from 'next/image'; +import {useEffect,useMemo,useState} from 'react'; +import {ArrowUpRight,ShieldCheck,Smartphone} from 'lucide-react'; +import {Card,CardContent} from '@/components/ui/card'; +import {Button} from '@/components/ui/button'; +import {Tabs,TabsList,TabsTrigger} from '@/components/ui/tabs'; +import {Dialog,DialogContent,DialogHeader,DialogTitle,DialogTrigger} from '@/components/ui/dialog'; +import type {PublicClient,PublicShowcaseConfig} from '@/shared/contracts'; +import {flowGroups,labelKey,needsPhone,type Tab} from '@/shared/flow-catalog'; +import {PhoneInput,type PhoneValue} from '@/features/phone/phone-input'; +import {QrDialog} from '@/features/qr/qr-dialog'; +import {ServerSelect} from './server-select'; +import {InfoDialogs} from './info-dialogs'; +import {LoginError} from './login-error'; +import {usePreferences} from './use-preferences'; +export function Showcase({config}:{config:PublicShowcaseConfig}){ + const tabs=useMemo(()=>(Object.keys(flowGroups) as Tab[]).filter(tab=>flowGroups[tab].some(flow=>config.clients.some(c=>c.user_flow===flow))),[config.clients]); + const servers=useMemo(()=>config.auth_servers.map(s=>s.id),[config.auth_servers]);const preferences=usePreferences(tabs,servers); + const [phone,setPhone]=useState({number:'',valid:false,country:config.defaultCountry||'rs'}),[country,setCountry]=useState(config.defaultCountry||'rs'),[mobile,setMobile]=useState(false),[error,setError]=useState(''),[qr,setQr]=useState<{state:string;url:string;title:string}|null>(null); + useEffect(()=>{const resize=()=>setMobile(window.innerWidth<=768||/Android|iPhone|Mobile/i.test(navigator.userAgent));resize();window.addEventListener('resize',resize);if(!config.defaultCountry&&config.appEnv!=='stage')fetch('/api/geoip').then(r=>r.json()).then(data=>setCountry(data.country||'rs')).catch(()=>{});return()=>window.removeEventListener('resize',resize);},[config.defaultCountry,config.appEnv]); + function start(client:PublicClient){ + setError('');if(!preferences.serverId){setError('Please select an auth server');return;}if(needsPhone(client.user_flow)&&!phone.valid){setError(config.locale.modals.phone_invalid);return;} + if(client.user_flow==='pvn_sms'||client.user_flow==='pvn_sim'||client.user_flow==='login_sim'){setError('This flow is being migrated.');return;} + const isQr=!mobile&&!['pvn_im','login_im'].includes(client.user_flow),state=crypto.randomUUID()+(isQr?'-qrcode':''); + const params=new URLSearchParams({user_flow:client.user_flow,server_id:preferences.serverId,state});if(needsPhone(client.user_flow))params.set('phone',phone.number); + const url=new URL('/auth/start?'+params,location.origin).href; + if(isQr)setQr({state,url,title:client.title||'Verify your identity'});else location.assign(url); + } + const section=config.locale.sections[preferences.tab],flows=flowGroups[preferences.tab]||[]; + return
+
+
+
Mobile identity{config.logoUrl?{config.locale.app.name}:null}

{config.locale.app.name}

Explore phone verification, quick access and identity services through your mobile network.

Powered by IPification
+ {preferences.selectTab(v as Tab);setPhone({number:'',valid:false,country});setError('');setQr(null);}}>{tabs.map(tab=>{config.locale.tabs[tab]})}

{section?.title}

{section?.description}

{preferences.tab!=='login'&&}
{flows.map(flow=>{const client=config.clients.find(c=>c.user_flow===flow);if(!client||flow==='pvn_ipificator'&&!mobile)return null;const key=labelKey(flow);return ;})}
{error&&

{error}

}

Choose a method to explore the authentication flow.

+
{phone.country==='vn'&&

{config.locale.footer.viettel_legal}

{config.locale.footer.viettel}Viettel consent
", - showConfirmButton: false, - showCloseButton: true, - // heightAuto: false - }); -} - -function initPhoneInput(input) { - showViettelLegal(countryCode); - if (!preferredCountries.includes(countryCode)) preferredCountries.push(countryCode); - - window.intlTelInput(input, { - allowDropdown: !disabledSelectCountry, - countrySearch: !disabledSelectCountry, - formatOnDisplay: true, - showSelectedDialCode: true, - initialCountry: app_env === 'stage' ? 'ww' : countryCode, - // geoIpLookup: function (success, failure) { - // success(countryCode); - // }, - customPlaceholder: function (selectedCountryPlaceholder, selectedCountryData) { - return selectedCountryData.name === 'Wonderland' ? '123456789' : selectedCountryPlaceholder; - }, - preferredCountries: preferredCountries, - separateDialCode: true, - // utilsScript: '/js/lib/intl-tel-input-20.0.5/js/utils.js', - }); - - input.addEventListener('countrychange', function () { - var itic = window.intlTelInput.getInstance(input); - const country = itic.getSelectedCountryData(); - showViettelLegal(country.iso2); - }); -} - -function randomstring(L) { - var s = ''; - var randomchar = function () { - var n = Math.floor(Math.random() * 62); - if (n < 10) return n; //1-10 - if (n < 36) return String.fromCharCode(n + 55); //A-Z - return String.fromCharCode(n + 61); //a-z - }; - while (s.length < L) s += randomchar(); - return s; -} - -function goto_link(url) { - $('.wrapper-loader').addClass('show'); - window.location.href = url; -} - -function choose_option(selector) { - $('.btn').removeClass('btn-active'); - $('.btn_' + selector).addClass('btn-active'); -} - -function attachWebOtpToInput(getInput, onCodeReceived) { - if (!window.isSecureContext || !('OTPCredential' in window) || !('credentials' in navigator)) { - return null; - } - - const ac = new AbortController(); - - navigator.credentials.get({ - otp: { transport: ['sms'] }, - signal: ac.signal - }).then((otp) => { - const input = getInput(); - if (!input || !otp || !otp.code) { - return; - } - - input.value = otp.code; - input.dispatchEvent(new Event('input', { bubbles: true })); - onCodeReceived(otp.code); - }).catch((error) => { - if (error && error.name !== 'AbortError') { - console.log('WebOTP error', error.message || error); - } - }); - - return ac; -} - -function sanitizeOtpValue(value) { - return (value || '').replace(/\D/g, ''); -} - -$(document).ready(function () { - // Initialize auth server dropdown - initAuthServerDropdown(); - - fetchCountryCode(function(){ - // country phone setting library - document.querySelectorAll('.phoneNumber').forEach((el) => { - initPhoneInput(el); - }); - }) - - if(isMobile) { - $('#pvn_ipificator').css('display', 'block'); - } - - - $('.info-icon').on('click', function () { - $('#app_info').modal('show'); - }); - - $('.btn-user-flow').click(function () { - var user_flow = $(this).data('user-flow'); - var client_id = $(this).data('client-id'); - var phone_number; - var dialCode; - - if (['pvn_ip', 'pvn_ip_plus', 'pvn_im', 'kyc_phone', 'pvn_ipificator', 'pvn_sim', 'pvn_sms'].indexOf(user_flow) >= 0) { - var parent = $(this).closest('.block-button'); - var inputPhone = parent.find('input.phoneNumber'); - - if (inputPhone.length >= 0) { - var iti = window.intlTelInput.getInstance(inputPhone[0]); - phone_number = iti.getNumber(); - dialCode = iti.getSelectedCountryData().dialCode - phone_number = (phone_number || '').replace(/[ +]/g, ''); - - if (!phone_number || phone_number == '') { - $('.wrapper-loader').removeClass('show'); - $('#input_alert').modal('show'); - return; - } - - if (!iti.isValidNumber() && dialCode !== '999') { - $('.wrapper-loader').removeClass('show'); - $('#phone_invalid_alert').modal('show'); - return; - } - } - } - - if(user_flow === 'pvn_sim') { - start_pvn_sim(client_id, phone_number); - return; - } - - if(user_flow === 'pvn_sms') { - start_pvn_sms(client_id, phone_number); - return; - } - - if(user_flow === 'login_sim') { - start_login_sim(client_id); - return; - } - - localStorage.setItem('selector', location.hash.substring(1)); - var data_title = $(this).attr('data-title'); - - // Get selected server_id - var serverId = getSelectedServerId(); - if (!serverId) { - alert('Please select an auth server'); - return; - } - - var params = new URLSearchParams({ - user_flow: user_flow, - server_id: serverId, - }); - - if (phone_number) params.set('phone', phone_number); - - var redirectURL = base_url + '/auth/start'; - - if (isMobile || user_flow === 'pvn_im' || user_flow === 'login_im') { - params.set('state', randomstring(10)); - redirectURL += '?' + params.toString(); - // console.log('redirectURL', redirectURL); - goto_link(redirectURL); - } else { - // state format "randomstring-qrcode" and we will know use qr code or not - var state = randomstring(10) + '-' + 'qrcode'; - params.set('state', state); - redirectURL += '?' + params.toString(); - console.log('QR URL', redirectURL); - showQrcodeWithLink(data_title, redirectURL, state); - } - }); - - async function start_pvn_sim(client_id, phone_number) { - console.log('start_pvn_sim'); - - const serverId = getSelectedServerId(); - if (!serverId) { - alert('Please select an auth server'); - return; - } - - const data = { - "login_hint": phone_number, - "carrier_hint": 51010, - "client_id": client_id, - "scope": "openid ip:phone_verify", - "server_id": serverId - } - - start_ts43_flow(data); - } - - async function start_pvn_sms(client_id, phone_number) { - console.log('start_pvn_sms'); - - const serverId = getSelectedServerId(); - if (!serverId) { - alert('Please select an auth server'); - return; - } - - const data = { - "login_hint": phone_number, - "client_id": client_id, - "scope": "openid ip:phone_verify", - "server_id": serverId - } - - start_sms_flow(data); - } - - async function start_login_sim(client_id) { - console.log('start_login_sim'); - - const serverId = getSelectedServerId(); - if (!serverId) { - alert('Please select an auth server'); - return; - } - - const data = { - "login_hint": "anonymous", - "carrier_hint": 51010, - "client_id": client_id, - "operation": "GetPhoneNumber", - "scope": "openid ip:phone", - "server_id": serverId - } - - start_ts43_flow(data); - } - - async function start_ts43_flow(data) { - try { - const response = await fetch('/ts43/auth', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(data), - }); - - if(!response.ok) { - alert('AuthError: ' + response.statusText); - return; - } - - const body = await response.json(); - const { auth_req_id, digital_request, nonce } = body; - - try { - var credentialResponse = await navigator.credentials.get({ - digital: { - requests: [digital_request] - }, - }) - - // var credentialResponse = { - // data: { - // vp_token: { - // 'ipification.com': [ - // '1234567890' - // ] - // } - // } - // } - - if(credentialResponse == null) { - alert('Response is null') - } else { - const credentialData = credentialResponse.token || credentialResponse.data - - const { vp_token } = credentialData || {} - const dataToken = { - "vp_token": vp_token['ipification.com'][0], - "auth_req_id": auth_req_id, - "client_id": data.client_id, - "nonce": nonce, - } - - const response = await fetch('/ts43/token', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(dataToken), - }); - - const body = await response.json(); - - log_data({ - type: 'get_token_response', - data: body, - }); - - window.location.href = '/user/info'; - } - } catch(error) { - alert(error.message); - } - } catch (error) { - alert(error.message); - console.log('error', error.message); - } - } - - async function start_sms_flow(data) { - try { - const response = await fetch('/sms/auth', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(data), - }); - - if(!response.ok) { - alert('AuthError: ' + response.statusText); - return; - } - - const body = await response.json(); - const { auth_req_id, nonce } = body; - let otpAbortController = null; - - const otpResult = await Swal.fire({ - title: 'Enter OTP code', - input: 'text', - inputPlaceholder: 'OTP code', - confirmButtonText: 'Enter', - showCancelButton: true, - cancelButtonText: 'Cancel', - heightAuto: false, - showLoaderOnConfirm: true, - allowOutsideClick: () => !Swal.isLoading(), - customClass: { - popup: 'sms-otp-popup', - title: 'sms-otp-title', - input: 'sms-otp-input', - actions: 'sms-otp-actions', - confirmButton: 'sms-otp-confirm', - cancelButton: 'sms-otp-cancel', - }, - inputAttributes: { - autocomplete: 'one-time-code', - autocapitalize: 'off', - autocorrect: 'off', - inputmode: 'numeric', - pattern: '[0-9]*' - }, - didOpen: () => { - const input = Swal.getInput(); - if (input) { - input.addEventListener('input', function () { - const sanitizedValue = sanitizeOtpValue(this.value); - if (this.value !== sanitizedValue) { - this.value = sanitizedValue; - } - }); - } - - otpAbortController = attachWebOtpToInput( - () => Swal.getInput(), - () => { - if (!Swal.isLoading()) { - Swal.clickConfirm(); - } - } - ); - }, - willClose: () => { - if (otpAbortController) { - otpAbortController.abort(); - } - }, - inputValidator: (value) => { - const sanitizedValue = sanitizeOtpValue(value); - if (!sanitizedValue) { - return 'Please enter OTP code'; - } - }, - preConfirm: async (otpCode) => { - const dataToken = { - "code": sanitizeOtpValue(otpCode), - "auth_req_id": auth_req_id, - "client_id": data.client_id, - "nonce": nonce, - "server_id": data.server_id, - }; - - try { - const tokenResponse = await fetch('/sms/token', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(dataToken), - }); - - const tokenBody = await tokenResponse.json(); - - await log_sms_data({ - type: 'get_token_response', - data: tokenBody, - }); - - if (!tokenResponse.ok) { - Swal.showValidationMessage(tokenBody.data?.error_description || tokenBody.error || 'Unable to verify OTP code'); - return false; - } - - return tokenBody; - } catch (error) { - Swal.showValidationMessage(error.message || 'Unable to verify OTP code'); - return false; - } - } - }); - - if (!otpResult.isConfirmed) { - return; - } - - window.location.href = '/user/info'; - } catch (error) { - alert(error.message); - console.log('error', error.message); - } - } - - async function log_data(data) { - await fetch('/ts43/log', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ data: data }), - }); - } - - async function log_sms_data(data) { - await fetch('/sms/log', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ data: data }), - }); - } - - $('#select').on('change', function () { - var url = $(this).val(); - if (url) { - window.location.href = url; - } - }); - - $('input[name=phone]').on('input', function (e) { - var phone = $(this) - .val() - .replace(/[^0-9]/g, ''); - $(this).val(phone); - if (phone && phone != '') { - $('.block-pnv').addClass('block-active'); - } else { - $('.block-pnv').removeClass('block-active'); - } - }); - - $('input[name=phone_kyc]').on('input', function (e) { - var phone = $(this) - .val() - .replace(/[^0-9]/g, ''); - $(this).val(phone); - if (phone && phone != '') { - $('.block-identity').addClass('block-active'); - } else { - $('.block-identity').removeClass('block-active'); - } - }); - - if ($('#response_info').length > 0) { - $('#response_info').modal('show'); - } - - $('#btnQrCode').on('click', function () { - $('#qrcode').modal('show'); - }); - - if ($('#error_alert').length > 0) { - $('#error_alert').modal('show'); - } - let hash = window.location.hash; - const item = localStorage.getItem('selector'); - - if (hash) { - localStorage.removeItem('selector'); - window.location.href = hash; - select_nav(hash.substring(1)); - } else if (!item) { - location.hash = '#pnv'; - } else { - window.location.href = '#' + item; - select_nav(item); - } -}); - -// console.log('window.hash', window.location.hash); - -function select_nav(selector) { - $('.phoneNumber').val(''); - $('.block-button').removeClass('block-active'); - $('.nav-link').removeClass('active'); - $('.nav-link-' + selector).addClass('active'); - $('.block').removeClass('active'); - $('.block-' + selector).addClass('active'); - localStorage.setItem('selector', selector); -} - -$('#help').click(() => { - const desktop = $(window).width() >= 576; - if (desktop) { - window.open( - 'https://www.ipification.com/media/IPification%20-%20Showcase%20Guide%202.2.pdf', - 'wwww', - 'width=' + - parseInt(window.innerWidth) * 0.37 + - ',height=' + - parseInt(window.innerHeight) * 1 + - 'left=0,top=0', - ); - } else { - let newUrl = 'https://www.ipification.com/media/IPification%20-%20Showcase%20Guide%202.2.pdf'; - $('#help').attr('href', newUrl); - $('#help').attr('target', '_blank'); - } -}); - -function showQrcodeWithLink(title, url, state) { - // add event listener when scan qr code successful - start_session_listener(state); - - Swal.fire({ - title: title, - html: "
Scan QR code with your phone
", - showConfirmButton: false, - showCloseButton: true, - heightAuto: false, - customClass: { - popup: 'qrcode-popup', - }, - didOpen: () => { - var qrCodeElm = document.querySelector('.qr-code-block .qrcode-img'); - new QRCode(qrCodeElm, { - text: url, - width: 212, - height: 212, - correctLevel: QRCode.CorrectLevel.L, - }); - qrCodeElm.setAttribute('title', ''); - }, - }); -} diff --git a/public/js/socket.js b/public/js/socket.js deleted file mode 100644 index 6be57e8..0000000 --- a/public/js/socket.js +++ /dev/null @@ -1,26 +0,0 @@ -(function () { - var start_session_listener = function(state){ - - var socket = io(); - - socket.on('connect', () => { - // emit socket server to join state channel - socket.emit('init', { state: state }) - }) - - // listener messages from socket server - socket.on('messages', (response) => { - // console.log('socket response: ', response); - var event_name = response.event_name; - - if(event_name == 'url'){ - window.location.href = response.url; - } - }); - } - - if(typeof(window.start_session_listener) === 'undefined'){ - window.start_session_listener = start_session_listener; - } - -})(); \ No newline at end of file diff --git a/routes/auth.js b/routes/auth.js deleted file mode 100644 index 1c4e079..0000000 --- a/routes/auth.js +++ /dev/null @@ -1,21 +0,0 @@ -const express = require('express'); -const web = require('./auth/webHandlers'); -const api = require('./auth/apiHandlers'); - -const router = express.Router(); - -// Browser authentication: authorize, receive the provider callback, adopt the session. -router.get('/login', web.login); -router.get('/start', web.start); -router.get('/callback/:userFlow/:serverId', web.callback); -router.get('/complete', web.complete); - -// Result pages shown on the phone after a desktop QR authentication. -router.get('/qrcode/complete', web.qrComplete); -router.get('/qrcode/error', web.qrError); - -// JSON APIs for backchannel result lookup and direct mobile code exchange. -router.post('/s2s/signin', api.s2sSignin); -router.post('/mobile/login', api.mobileLogin); - -module.exports = router; diff --git a/routes/auth/apiHandlers.js b/routes/auth/apiHandlers.js deleted file mode 100644 index db5eae5..0000000 --- a/routes/auth/apiHandlers.js +++ /dev/null @@ -1,74 +0,0 @@ -const authResultStore = require('../../services/authResultStore'); -const { exchangeAuthorizationCode } = require('../../services/authService'); -const logger = require('../../utils/logger'); -const { findClientByClientId } = require('../../utils/helpers'); -const { ERROR_MESSAGES, HTTP_STATUS } = require('../../utils/constants'); - -/** - * Return raw userinfo stored by a callback for the body state. - * Return empty 401 when no result exists; leave the cached result available. - */ -async function s2sSignin(req, res) { - const { state } = req.body || {}; - logger.log(`body: ${JSON.stringify(req.body || {})}`); - const userinfo = await authResultStore.getUserInfo(state); - - if (userinfo) { - // Demo response; an integrating application can issue its own token here. - res.send(userinfo); - } else { - res.status(HTTP_STATUS.UNAUTHORIZED).send(); - } -} - -/** - * Exchange body code using client_id, redirect_uri, and optional server_id. - * Return auth_server merged with userinfo directly, without a browser session or cache write. - * Invalid server returns 400; unknown client or exchange failure returns 401. - */ -async function mobileLogin(req, res) { - const { client_id, code, redirect_uri, server_id: serverId } = req.body || {}; - logger.log(`body: ${JSON.stringify(req.body || {})}`); - - const { clients, getAuthServer, realm } = res.locals; - - // Get auth server (defaults to first server if serverId not provided) - const authServer = getAuthServer(serverId); - if (!authServer) { - const error_message = serverId ? `Invalid server_id: '${serverId}'` : 'No auth servers configured'; - logger.error(error_message); - res.status(HTTP_STATUS.BAD_REQUEST).send({ error: error_message }); - return; - } - - const client = findClientByClientId(clients, client_id); - if (!client) { - logger.error(ERROR_MESSAGES.CLIENT_NOT_FOUND); - res.status(HTTP_STATUS.UNAUTHORIZED).send({ error: ERROR_MESSAGES.CLIENT_NOT_FOUND }); - return; - } - - try { - const { userInfo } = await exchangeAuthorizationCode({ - server: authServer, - realm, - clientId: client_id, - clientSecret: client.client_secret, - redirectUri: redirect_uri, - code, - }); - logger.log(userInfo); - - const tokenResponse = { - auth_server: authServer, - ...userInfo, - }; - - res.send(tokenResponse); - } catch (err) { - logger.error('---> get token error:', err.message); - res.status(HTTP_STATUS.UNAUTHORIZED).send({ error: err.message }); - } -} - -module.exports = { s2sSignin, mobileLogin }; diff --git a/routes/auth/webHandlers.js b/routes/auth/webHandlers.js deleted file mode 100644 index c5a50f3..0000000 --- a/routes/auth/webHandlers.js +++ /dev/null @@ -1,194 +0,0 @@ -const { v4: uuidv4 } = require('uuid'); -const htmlEntities = require('html-entities'); -const prettyHtml = require('json-pretty-html').default; -const authResultStore = require('../../services/authResultStore'); -const { buildAuthorizationUrl, exchangeAuthorizationCode } = require('../../services/authService'); -const logger = require('../../utils/logger'); -const { findClientByUserFlow } = require('../../utils/helpers'); -const { QR_CODE_SUFFIX, ERROR_MESSAGES, HTTP_STATUS } = require('../../utils/constants'); - -const CALLBACK_ERROR_MESSAGES = Object.freeze({ - access_denied: 'Authentication was cancelled.', - login_required: 'Authentication is required to continue.', - operator_not_resolved: 'Your mobile operator could not be resolved.', - server_error: 'Authentication is temporarily unavailable.', - temporarily_unavailable: 'Authentication is temporarily unavailable.', -}); - -/** - * Render the login page and display the pending session error once. - * Clear the error from the session before rendering. - */ -function login(req, res) { - const error_message = req.session.error_message; - - req.session.error_message = null; - res.render('login', { - error_message: htmlEntities.encode(error_message), - node_env: process.env.NODE_ENV - }); -} - -/** - * Redirect to the provider for query user_flow and state, with optional phone/server_id. - * Default to the first configured server; return to login when state is absent. - */ -function start(req, res) { - const { clients, getAuthServer, realm, baseUrl } = res.locals; - const { user_flow: userFlow, phone, state, server_id: serverId } = req.query || {}; - - // Get auth server (defaults to first server if serverId not provided) - const authServer = getAuthServer(serverId); - if (!authServer) { - res.status(HTTP_STATUS.BAD_REQUEST).json({ - error: serverId ? 'INVALID_SERVER_ID' : 'NO_AUTH_SERVERS_CONFIGURED', - }); - return; - } - - const client = findClientByUserFlow(clients, userFlow); - if (!client) { - res.send(ERROR_MESSAGES.CLIENT_NOT_FOUND); - return; - } - - if (!state || state === '') { - res.redirect('/auth/login'); - return; - } - - const authUrl = buildAuthorizationUrl({ client, baseUrl, userFlow, server: authServer, realm, state, phone }); - - logger.log('authUrl:', authUrl); - res.redirect(authUrl); -} - -function isQrCodeState(state) { - return state.indexOf(QR_CODE_SUFFIX) >= 0; -} - -function handleQrCodeCallback(req, res, state, authCompleteUrl, userInfo) { - const channel = `auth:${state}`; - req.app.get('socket').to(channel).emit('messages', { - event_name: 'url', - url: authCompleteUrl - }); - - if (userInfo.phone_number_verified === 'false') { - res.redirect('/auth/qrcode/error'); - } else { - res.redirect('/auth/qrcode/complete'); - } -} - -/** - * Exchange the provider code for userinfo using the path's userFlow/serverId. - * Store display data and raw userinfo before selecting the response: - * backchannel returns empty 200; QR notifies the desktop and redirects the phone; - * ordinary web authentication redirects to session completion. - * Provider-reported errors return to login with a fixed session error message. - */ -async function callback(req, res) { - const { userFlow, serverId } = req.params || {}; - const { state = uuidv4(), code } = req.query || {}; - const { clients, getAuthServer, realm, baseUrl } = res.locals; - const ipBackchannelAuth = req.headers['ip-backchannel-im-auth'] === 'true'; - - if (req.query.error || req.query.error_description) { - req.session.error_message = CALLBACK_ERROR_MESSAGES[req.query.error] || 'Unable to complete authentication.'; - res.redirect(`/auth/login`); - return; - } - - // Get auth server (defaults to first server if serverId not provided) - const authServer = getAuthServer(serverId); - if (!authServer) { - res.status(HTTP_STATUS.BAD_REQUEST).send(serverId ? `Invalid server_id: '${serverId}'` : 'No auth servers configured'); - return; - } - - const client = findClientByUserFlow(clients, userFlow); - if (!client) { - res.send(ERROR_MESSAGES.CLIENT_NOT_FOUND); - return; - } - - const { client_id: clientId, client_secret: clientSecret, title: pageTitle } = client; - - const redirectUri = `${baseUrl}/auth/callback/${userFlow}/${authServer.id}`; - - try { - const { userInfo } = await exchangeAuthorizationCode({ - server: authServer, realm, clientId, clientSecret, redirectUri, code, - }); - logger.log(`state: ${state}`); - logger.log(`userInfo: ${JSON.stringify(userInfo)}`); - - const response = { - userInfo: prettyHtml(userInfo), - client_id: clientId, - client_title: pageTitle, - state - }; - - await authResultStore.saveResult(state, response, userInfo); - const auth_complete_url = `${baseUrl}/auth/complete?state=${state}`; - - // Backchannel responses take precedence even when state contains the QR marker. - if (ipBackchannelAuth) { - logger.log('ipBackchannelAuth: true -> render 200'); - res.send(); - return; - } - - // The provider code is already exchanged and both results are stored. - // Notify the desktop to finish its session through /auth/complete. - if (isQrCodeState(state)) { - handleQrCodeCallback(req, res, state, auth_complete_url, userInfo); - return; - } - - res.redirect(auth_complete_url); - } catch (err) { - logger.error('---> get token error:', err.message); - if (err.response?.data) { - logger.error('Error response data:', err.response.data); - } - res.status(HTTP_STATUS.BAD_REQUEST).send(err.message); - } -} - -/** - * Wait for the callback result identified by query state, then adopt it into the session. - * Redirect to /user/info on success, or / when polling finds no result. - */ -async function complete(req, res) { - const { state } = req.query || {}; - - const response = await authResultStore.waitForResult(state); - - if (response) { - // The desktop or mobile browser adopts the stored display result. - req.session.isAuthenticated = true; - req.session.userData = response; - res.redirect('/user/info'); - return; - } - res.redirect('/'); -} - -/** - * Render QR success on the phone; the desktop completes its session separately. - */ -async function qrComplete(req, res) { - res.render('qr_success'); -} - -/** - * Render the phone's QR error page when verification returns the string 'false'. - */ -async function qrError(req, res) { - res.render('qr_error'); -} - -module.exports = { login, start, callback, complete, qrComplete, qrError }; diff --git a/routes/index.js b/routes/index.js deleted file mode 100644 index f4d8c52..0000000 --- a/routes/index.js +++ /dev/null @@ -1,61 +0,0 @@ -const express = require('express'); -const geoip = require('geoip-lite'); -const path = require('path'); -const fs = require('fs'); -const config = require('config'); -const logger = require('../utils/logger'); -const { version } = require('../package.json'); - -const router = express.Router(); - -const { deepMerge } = require('../utils/helpers'); - -// Load app_config.json and merge with app_config from default.json if exists -const appConfigPath = path.join(__dirname, '..', 'config', 'app_config.json'); -const defaultAppConfig = JSON.parse(fs.readFileSync(appConfigPath, 'utf8')); - -const appConfig = config.app_config ? deepMerge(defaultAppConfig, config.app_config) : defaultAppConfig; - -router.get('/', async (req, res) => { - res.redirect('/auth/login'); -}); - -router.get('/api/geoip', async (req, res) => { - const default_country_code = res.locals.default_country_code || 'rs'; - try { - const ip = req.ip; - logger.log('client ip', ip); - const geo = geoip.lookup(ip); - - res.send({ - country: (geo ? geo.country : default_country_code).toLowerCase(), - ip - }); - } catch (error) { - logger.error('GeoIP lookup error:', error); - res.send({ - country: default_country_code, - ip: req.ip - }); - } -}); - -router.get('/api/config', (req, res) => { - const { auth_servers, realm, clients, baseUrl } = res.locals; - const safeClients = Array.isArray(clients) - ? clients.map(({ client_secret, ...client }) => ({ - ...client, - redirect_uri: `${baseUrl}/auth/callback/${client.user_flow}` - })) - : []; - - res.send({ - version, - auth_servers, - realm, - clients: safeClients, - app_config: appConfig - }); -}); - -module.exports = router; diff --git a/routes/playIntegrity.js b/routes/playIntegrity.js deleted file mode 100644 index c5f9845..0000000 --- a/routes/playIntegrity.js +++ /dev/null @@ -1,322 +0,0 @@ -const express = require('express'); -const { v4: uuidv4 } = require('uuid'); -const { PlayIntegrityUnavailableError } = require('../services/playIntegrityService'); -const logger = require('../utils/logger'); - -function createPlayIntegrityRouter({ - attemptService, - verify, - exchangeCodeAndGetUserInfo, - expectedPackageName, - userFlow, - bypassVerification = false, - verifyRequiredForRequest, - requestIdFactory = uuidv4, - logCompletion = defaultLogCompletion, - logVerificationFailure = defaultLogVerificationFailure, -}) { - if (!attemptService || typeof attemptService.completeTransaction !== 'function' - || typeof verify !== 'function' || typeof exchangeCodeAndGetUserInfo !== 'function') { - throw new TypeError('attempt, verification, and exchange services are required'); - } - if (!isNonEmptyString(userFlow)) { - throw new TypeError('a Play Integrity user flow is required'); - } - - const router = express.Router(); - router.use(express.json({ limit: '16kb' })); - - router.post('/attempt', async (req, res) => { - const completion = createCompletion('attempt', requestIdFactory); - const { - phone_number: phoneNumber, - server_id: serverId, - } = req.body || {}; - if (!isNonEmptyString(phoneNumber) || !isNonEmptyString(serverId)) { - respond(res, 400, safeResponse(completion.requestId, 'deny', ['INVALID_REQUEST']), completion, logCompletion); - return; - } - - try { - const resolveClient = createClientResolver(res.locals, serverId, userFlow); - const client = resolveClient(); - if (!client || !isNonEmptyString(client.client_id)) { - throw new TypeError('configured Play Integrity client is unavailable'); - } - const result = await attemptService.createAttempt({ - phoneNumber, - clientId: client.client_id, - serverId, - resolveClient, - resolveServer: createServerResolver(res.locals), - }); - if (!result) { - respond(res, 409, safeResponse(completion.requestId, 'deny', ['ATTEMPT_UNAVAILABLE']), completion, logCompletion); - return; - } - respond(res, 201, publicAttemptResponse(result), completion, logCompletion, 'allow'); - } catch { - respond(res, 400, safeResponse(completion.requestId, 'deny', ['INVALID_REQUEST']), completion, logCompletion); - } - }); - - router.post('/verify', async (req, res) => { - const completion = createCompletion('verify', requestIdFactory); - const { - attempt_id: attemptId, - integrity_token: integrityToken, - } = req.body || {}; - if (!isNonEmptyString(attemptId) || !isNonEmptyString(integrityToken)) { - respond(res, 400, safeResponse(completion.requestId, 'deny', ['INVALID_REQUEST']), completion, logCompletion); - return; - } - - const attempt = await attemptService.claimAttempt(attemptId); - if (!attempt) { - respond(res, 409, safeResponse(completion.requestId, 'deny', ['ATTEMPT_UNAVAILABLE']), completion, logCompletion); - return; - } - - try { - const shouldVerify = typeof verifyRequiredForRequest === 'function' - ? verifyRequiredForRequest(req) - : !bypassVerification; - completion.verificationMode = shouldVerify ? 'verify' : 'bypass'; - const verdict = !shouldVerify - ? { decision: 'allow', reasonCodes: [] } - : await verify({ integrityToken, expectedRequestHash: attempt.requestHash, expectedPackageName }); - const reasonCodes = safeReasonCodes(verdict?.reasonCodes); - if (verdict?.decision !== 'allow') { - await attemptService.rejectAttempt(attempt.id, reasonCodes[0] || 'POLICY_DENIED'); - respond(res, 403, safeResponse(completion.requestId, 'deny', reasonCodes), completion, logCompletion); - return; - } - - const transaction = await attemptService.approveAttempt(attempt.id); - if (!transaction) { - respond(res, 409, safeResponse(completion.requestId, 'deny', ['ATTEMPT_UNAVAILABLE']), completion, logCompletion); - return; - } - const state = attemptService.createSignedState(transaction); - respond(res, 201, { state, expires_at: transaction.expiresAt }, completion, logCompletion, 'allow'); - } catch (error) { - if (error instanceof PlayIntegrityUnavailableError) { - logVerificationFailureSafely(logVerificationFailure, completion.requestId, error.reasonCode, error, integrityToken); - await rejectAttemptSafely(attemptService, attempt.id, error.reasonCode); - respond(res, 503, safeResponse(completion.requestId, 'unavailable', [error.reasonCode]), completion, logCompletion); - return; - } - logVerificationFailureSafely(logVerificationFailure, completion.requestId, 'GOOGLE_VERIFICATION_FAILED', error, integrityToken); - await rejectAttemptSafely(attemptService, attempt.id, 'GOOGLE_VERIFICATION_FAILED'); - respond(res, 503, safeResponse(completion.requestId, 'unavailable', ['GOOGLE_VERIFICATION_FAILED']), completion, logCompletion); - } - }); - - router.post('/token-exchange', async (req, res) => { - const completion = createCompletion('token-exchange', requestIdFactory); - const { code, state } = req.body || {}; - if (!isNonEmptyString(code) || !isWellFormedState(state)) { - respond(res, 400, safeResponse(completion.requestId, 'deny', ['INVALID_REQUEST']), completion, logCompletion); - return; - } - - const claim = await attemptService.claimTransaction(state); - if (claim?.status === 'invalid') { - respond(res, 400, safeResponse(completion.requestId, 'deny', ['INVALID_STATE']), completion, logCompletion); - return; - } - if (claim?.status !== 'claimed') { - respond(res, 409, safeResponse(completion.requestId, 'deny', ['TRANSACTION_UNAVAILABLE']), completion, logCompletion); - return; - } - const transaction = claim.transaction; - - try { - const { tokenUrl, userUrl, params } = exchangeInputs(transaction.attempt, code); - const { userInfo } = await exchangeCodeAndGetUserInfo(tokenUrl, userUrl, params); - await completeTransactionSafely(attemptService, transaction.id, 'consumed'); - respond(res, 200, { decision: 'allow', user_info: userInfo }, completion, logCompletion, 'allow'); - } catch { - await completeTransactionSafely(attemptService, transaction.id, 'failed'); - respond(res, 401, safeResponse(completion.requestId, 'deny', ['IPIFICATION_EXCHANGE_FAILED']), completion, logCompletion); - } - }); - - router.use((error, req, res, next) => { - if (error?.type === 'entity.parse.failed' || error?.type === 'entity.too.large') { - const completion = createCompletion(requestEndpoint(req), requestIdFactory); - respond(res, 400, safeResponse(completion.requestId, 'deny', ['INVALID_REQUEST']), completion, logCompletion); - return; - } - next(error); - }); - - return router; -} - -function createClientResolver(locals, serverId, userFlow) { - return () => { - const client = Array.isArray(locals.clients) - ? locals.clients.find((candidate) => candidate?.user_flow === userFlow) - : null; - if (!client) return null; - const redirectUri = client.redirect_uri || `${locals.baseUrl}/auth/callback/${client.user_flow}/${serverId}`; - return { ...client, id: client.client_id, redirectUri }; - }; -} - -function createServerResolver(locals) { - return (serverId) => { - const server = typeof locals.getAuthServer === 'function' ? locals.getAuthServer(serverId) : null; - if (!server) return null; - return { ...server, id: serverId, realm: locals.realm }; - }; -} - -function exchangeInputs(attempt, code) { - const client = attempt?.client; - const server = attempt?.server; - if (!isNonEmptyString(client?.client_id) || !isNonEmptyString(client?.client_secret) - || !isNonEmptyString(client?.redirectUri) || !isNonEmptyString(server?.url) - || !isNonEmptyString(server?.realm)) { - throw new TypeError('frozen attempt snapshot is incomplete'); - } - const root = server.url.replace(/\/$/, ''); - const protocolRoot = `${root}/realms/${encodeURIComponent(server.realm)}/protocol/openid-connect`; - return { - tokenUrl: `${protocolRoot}/token`, - userUrl: `${protocolRoot}/userinfo`, - params: { - code, - redirect_uri: client.redirectUri, - grant_type: 'authorization_code', - client_id: client.client_id, - client_secret: client.client_secret, - }, - }; -} - -function publicAttemptResponse(result) { - return { - attempt_id: result.attemptId, - request_hash: result.requestHash, - expires_at: result.expiresAt, - }; -} - -function isNonEmptyString(value) { - return typeof value === 'string' && value.trim().length > 0; -} - -function isWellFormedState(value) { - return typeof value === 'string' && /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(value); -} - -function safeReasonCodes(reasonCodes) { - const safe = Array.isArray(reasonCodes) - ? reasonCodes.filter((reason) => typeof reason === 'string' && /^[A-Z0-9_]{1,80}$/.test(reason)) - : []; - return safe.length > 0 ? safe : ['POLICY_DENIED']; -} - -function safeResponse(requestId, decision, reasonCodes) { - return { request_id: requestId, decision, reason_codes: safeReasonCodes(reasonCodes) }; -} - -function createCompletion(endpoint, requestIdFactory) { - return { endpoint, requestId: requestIdFactory(), startedAt: Date.now() }; -} - -function requestEndpoint(req) { - return req.path?.replace(/^\//, '') || 'unknown'; -} - -function respond(res, status, body, completion, logCompletion, decision = body.decision, reasonCodes = body.reason_codes || []) { - logCompletionSafely(logCompletion, { - requestId: completion.requestId, - endpoint: completion.endpoint, - ...(completion.verificationMode ? { verification_mode: completion.verificationMode } : {}), - decision, - reasonCodes: decision === 'allow' ? [] : safeReasonCodes(reasonCodes), - ...(decision === 'allow' ? {} : { reasonDetails: reasonDetailsForCodes(reasonCodes) }), - duration: Date.now() - completion.startedAt, - }); - res.status(status).json(body); -} - -function reasonDetailsForCodes(reasonCodes) { - const details = { - INVALID_REQUEST: 'Request payload is missing required fields or is malformed', - ATTEMPT_UNAVAILABLE: 'Attempt is expired, already claimed, or unavailable', - REQUEST_HASH_MISMATCH: 'Decoded request hash does not match the server-stored request hash', - PACKAGE_NAME_MISMATCH: 'Decoded package name does not match the configured package name', - APP_NOT_RECOGNIZED: 'App recognition verdict is not PLAY_RECOGNIZED', - DEVICE_INTEGRITY_NOT_MET: 'Device integrity verdict does not include MEETS_DEVICE_INTEGRITY', - APP_NOT_LICENSED: 'App licensing is required but the app is not LICENSED', - GOOGLE_CREDENTIALS_UNAVAILABLE: 'Backend Google credentials are unavailable', - GOOGLE_DECODE_FAILED: 'Google Play Integrity token decode failed', - GOOGLE_DECODE_TIMEOUT: 'Google Play Integrity token decode timed out', - INVALID_STATE: 'Signed state is invalid or cannot be verified', - TRANSACTION_UNAVAILABLE: 'Transaction is expired, already claimed, or unavailable', - IPIFICATION_EXCHANGE_FAILED: 'IPification authorization-code exchange failed', - POLICY_DENIED: 'Play Integrity policy denied the request', - }; - return safeReasonCodes(reasonCodes).map((reasonCode) => details[reasonCode] || 'Play Integrity policy rejected the request'); -} - -async function rejectAttemptSafely(attemptService, attemptId, reason) { - try { - await attemptService.rejectAttempt(attemptId, reason); - } catch { - // Dependency response takes priority over best-effort state bookkeeping. - } -} - -async function completeTransactionSafely(attemptService, transactionId, status) { - try { - await attemptService.completeTransaction(transactionId, status); - } catch { - // The transaction was already claimed, so completion bookkeeping cannot permit replay. - } -} - -function logCompletionSafely(logCompletion, event) { - try { - logCompletion(event); - } catch { - // Completion logging must not change the endpoint response. - } -} - -function defaultLogVerificationFailure(event) { - logger.error('Play Integrity verification failed', event); -} - -function logVerificationFailureSafely(logVerificationFailure, requestId, reasonCode, error, integrityToken) { - try { - const cause = error?.cause instanceof Error ? error.cause : error; - logVerificationFailure({ - request_id: requestId, - reason_code: reasonCode, - error_name: safeErrorName(cause), - error_message: safeErrorMessage(cause, integrityToken), - }); - } catch { - // Diagnostic logging must not change the endpoint response. - } -} - -function safeErrorName(error) { - return typeof error?.name === 'string' && error.name.length > 0 ? error.name.slice(0, 80) : 'Error'; -} - -function safeErrorMessage(error, integrityToken) { - if (typeof error?.message !== 'string' || error.message.length === 0) return 'unknown error'; - return error.message.slice(0, 500).replaceAll(integrityToken, '[REDACTED]'); -} - -function defaultLogCompletion(event) { - console.info(JSON.stringify(event)); -} - -module.exports = { createPlayIntegrityRouter, exchangeInputs }; diff --git a/routes/sms.js b/routes/sms.js deleted file mode 100644 index b6e1e88..0000000 --- a/routes/sms.js +++ /dev/null @@ -1,11 +0,0 @@ -const express = require('express'); -const handlers = require('./sms/handlers'); - -const router = express.Router(); - -// SMS authentication, client diagnostics, and session completion. -router.post('/auth', handlers.auth); -router.post('/log', handlers.log); -router.post('/token', handlers.token); - -module.exports = router; diff --git a/routes/sms/handlers.js b/routes/sms/handlers.js deleted file mode 100644 index 12d1b7e..0000000 --- a/routes/sms/handlers.js +++ /dev/null @@ -1,137 +0,0 @@ -const prettyHtml = require('json-pretty-html').default; -const logger = require('../../utils/logger'); -const { findClientByClientId } = require('../../utils/helpers'); -const { ERROR_MESSAGES, HTTP_STATUS } = require('../../utils/constants'); -const smsService = require('../../services/smsService'); - -/** - * Start SMS authentication from body client_id and optional server/scope hints. - * Return the request ID and nonce; preserve provider error status/data. - */ -async function auth(req, res) { - const { client_id: clientId, scope: reqScope, server_id: serverId, login_hint: loginHint } = req.body; - const { clients, getAuthServer, realm } = res.locals; - - const authServer = getAuthServer(serverId); - if (!authServer) { - res.status(HTTP_STATUS.BAD_REQUEST).send({ error: serverId ? `Invalid server_id: '${serverId}'` : 'No auth servers configured' }); - return; - } - - const { url: authServerUrl } = authServer; - const client = findClientByClientId(clients, clientId); - - if (!client) { - logger.log(`client(${clientId}) not found`); - res.status(HTTP_STATUS.UNAUTHORIZED).send(ERROR_MESSAGES.CLIENT_NOT_FOUND); - return; - } - - const { client_secret: clientSecret, scope } = client; - - try { - const { authReqId, nonce } = await smsService.startAuthentication({ - authServerUrl, realm, clientId, clientSecret, reqScope, scope, loginHint, - }); - - res.json({ - auth_server: authServer, - auth_req_id: authReqId, - nonce, - }); - } catch (error) { - logger.error('SMS auth error:', error.message); - - const errorResponse = { - auth_server: authServer, - success: false, - error: error.message, - status: error.response?.status || HTTP_STATUS.INTERNAL_SERVER_ERROR - }; - - if (error.response?.data) { - errorResponse.data = error.response.data; - } - - res.status(error.response?.status || HTTP_STATUS.INTERNAL_SERVER_ERROR).json(errorResponse); - } -} - -/** Record client diagnostics from body data and acknowledge with plain-text OK. */ -async function log(req, res) { - const { data } = req.body; - logger.log('--> sms log'); - logger.log(JSON.stringify(data)); - res.send('OK'); -} - -function buildSessionUserData(userInfo, client, nonce) { - return { - userInfo: prettyHtml(userInfo), - client_id: client.client_id, - client_title: client.title || 'SMS', - state: nonce, - }; -} - -/** - * Complete the SMS callback and CIBA exchange using body auth_req_id and code. - * Return userinfo and store display data in the browser session. - * Preserve the demo's session update on token failure, with formatted error data. - */ -async function token(req, res) { - logger.log('--> sms token'); - logger.log(JSON.stringify(req.body)); - const { auth_req_id: authReqId, code: smsCode, client_id: clientId, nonce, server_id: serverId } = req.body; - const { clients, getAuthServer, realm } = res.locals; - - const authServer = getAuthServer(serverId); - if (!authServer) { - res.status(HTTP_STATUS.BAD_REQUEST).send({ error: serverId ? `Invalid server_id: '${serverId}'` : 'No auth servers configured' }); - return; - } - - const { url: authServerUrl } = authServer; - const client = findClientByClientId(clients, clientId); - - if (!client) { - res.status(HTTP_STATUS.UNAUTHORIZED).send({ error: ERROR_MESSAGES.CLIENT_NOT_FOUND }); - return; - } - - const { client_secret: clientSecret } = client; - - try { - const userInfo = await smsService.exchangeToken({ - authServerUrl, realm, clientId, clientSecret, authReqId, smsCode, - }); - - req.session.isAuthenticated = true; - req.session.userData = buildSessionUserData(userInfo, client, nonce); - - res.json({ - auth_server: authServer, - ...userInfo, - }); - } catch (error) { - logger.error('SMS token error:', error.message); - - const errorResponse = { - error: error.message, - status: error.response?.status || HTTP_STATUS.INTERNAL_SERVER_ERROR - }; - - if (error.response?.data) { - errorResponse.data = error.response.data; - } - - logger.error('SMS token errorResponse:', errorResponse); - - req.session.isAuthenticated = true; - req.session.userData = buildSessionUserData(errorResponse, client, nonce); - - res.status(error.response?.status || HTTP_STATUS.INTERNAL_SERVER_ERROR).json(errorResponse); - } -} - -module.exports = { auth, log, token }; diff --git a/routes/ts43.js b/routes/ts43.js deleted file mode 100644 index 8961778..0000000 --- a/routes/ts43.js +++ /dev/null @@ -1,11 +0,0 @@ -const express = require('express'); -const handlers = require('./ts43/handlers'); - -const router = express.Router(); - -// TS43 authentication, client diagnostics, and session completion. -router.post('/auth', handlers.auth); -router.post('/log', handlers.log); -router.post('/token', handlers.token); - -module.exports = router; diff --git a/routes/ts43/handlers.js b/routes/ts43/handlers.js deleted file mode 100644 index 1eca1b6..0000000 --- a/routes/ts43/handlers.js +++ /dev/null @@ -1,145 +0,0 @@ -const prettyHtml = require('json-pretty-html').default; -const logger = require('../../utils/logger'); -const { findClientByClientId } = require('../../utils/helpers'); -const { ERROR_MESSAGES, HTTP_STATUS } = require('../../utils/constants'); -const ts43Service = require('../../services/ts43Service'); - -/** - * Start TS43 authentication from body client_id and optional server/scope hints. - * Return the request ID and nonce, plus the digital credential request; preserve provider error status/data. - */ -async function auth(req, res) { - const { login_hint, carrier_hint, client_id: clientId, operation, scope: reqScope, server_id: serverId } = req.body; - const { clients, getAuthServer, realm } = res.locals; - - // Get auth server (defaults to first server if serverId not provided) - const authServer = getAuthServer(serverId); - if (!authServer) { - res.status(HTTP_STATUS.BAD_REQUEST).send({ error: serverId ? `Invalid server_id: '${serverId}'` : 'No auth servers configured' }); - return; - } - - const { url: authServerUrl } = authServer; - - const client = findClientByClientId(clients, clientId); - - if (!client) { - logger.log(`client(${clientId}) not found`); - res.status(HTTP_STATUS.UNAUTHORIZED).send(ERROR_MESSAGES.CLIENT_NOT_FOUND); - return; - } - - const { client_secret: clientSecret, scope } = client; - - try { - const { authReqId, nonce, digitalRequest } = await ts43Service.startAuthentication({ - authServerUrl, realm, clientId, clientSecret, reqScope, scope, - loginHint: login_hint, carrierHint: carrier_hint, operation, - }); - - res.json({ - auth_server: authServer, - auth_req_id: authReqId, - nonce, - digital_request: digitalRequest, - }); - } catch (error) { - logger.error('CIBA Auth Error:', error.message); - - const errorResponse = { - auth_server: authServer, - success: false, - error: error.message, - status: error.response?.status || HTTP_STATUS.INTERNAL_SERVER_ERROR - }; - - if (error.response?.data) { - errorResponse.data = error.response.data; - } - - res.status(error.response?.status || HTTP_STATUS.INTERNAL_SERVER_ERROR).json(errorResponse); - } -} - -/** Record client diagnostics from body data and acknowledge with plain-text OK. */ -async function log(req, res) { - const { data: data } = req.body; - logger.log('--> log'); - logger.log(JSON.stringify(data)); - res.send('OK'); -} - -function buildSessionUserData(userInfo, clientId, nonce) { - return { - userInfo: prettyHtml(userInfo), - client_id: clientId, - client_title: 'SIM', - state: nonce, - }; -} - -/** - * Complete the TS43 callback and CIBA exchange using body auth_req_id and vp_token. - * Return userinfo and store display data in the browser session. - * Preserve the demo's session update on token failure, with formatted error data. - */ -async function token(req, res) { - logger.log('--> token'); - logger.log(JSON.stringify(req.body)); - const { vp_token: vpToken, auth_req_id: authReqId, client_id: clientId, nonce, server_id: serverId } = req.body; - const { clients, getAuthServer, realm } = res.locals; - - // Get auth server (defaults to first server if serverId not provided) - const authServer = getAuthServer(serverId); - if (!authServer) { - res.status(HTTP_STATUS.BAD_REQUEST).send({ error: serverId ? `Invalid server_id: '${serverId}'` : 'No auth servers configured' }); - return; - } - - const { url: authServerUrl } = authServer; - - const client = findClientByClientId(clients, clientId); - - if (!client) { - res.status(HTTP_STATUS.UNAUTHORIZED).send({ error: ERROR_MESSAGES.CLIENT_NOT_FOUND }); - return; - } - - const { client_secret: clientSecret } = client; - - try { - const userInfo = await ts43Service.exchangeToken({ - authServerUrl, realm, clientId, clientSecret, authReqId, vpToken, - }); - - req.session.isAuthenticated = true; - req.session.userData = buildSessionUserData(userInfo, clientId, nonce); - - const tokenResponse = { - auth_server: authServer, - ...userInfo, - }; - - res.json(tokenResponse); - } catch (error) { - logger.error('Token Auth Error:', error.message); - - const errorResponse = { - error: error.message, - status: error.response?.status || HTTP_STATUS.INTERNAL_SERVER_ERROR - }; - - if (error.response?.data) { - errorResponse.data = error.response.data; - } - - logger.error('Token Auth errorResponse:', errorResponse); - - req.session.isAuthenticated = true; - req.session.userData = buildSessionUserData(errorResponse, clientId, nonce); - - res.status(error.response?.status || HTTP_STATUS.INTERNAL_SERVER_ERROR).json(errorResponse); - } -} - -module.exports = { auth, log, token }; diff --git a/routes/user.js b/routes/user.js deleted file mode 100644 index 26d1518..0000000 --- a/routes/user.js +++ /dev/null @@ -1,20 +0,0 @@ -const express = require('express'); -const logger = require('../utils/logger'); - -const router = express.Router(); - -router.get('/info', async (req, res) => { - if (!req.session.isAuthenticated) { - res.redirect('/'); - return; - } - - try { - res.render('info', req.session.userData); - } catch (error) { - logger.error('Error rendering user info:', error); - res.redirect('/'); - } -}); - -module.exports = router; diff --git a/services/authResultStore.js b/services/authResultStore.js deleted file mode 100644 index 93f30a4..0000000 --- a/services/authResultStore.js +++ /dev/null @@ -1,36 +0,0 @@ -const dataStore = require('../lib/data_store'); -const { delay } = require('../utils/helpers'); -const { MAX_RETRY_ATTEMPTS, RETRY_DELAY_MS } = require('../utils/constants'); - -function createAuthResultStore({ dataStore, delay }) { - async function saveResult(state, response, userInfo) { - // Completion consumes the formatted response; S2S consumes raw userinfo. - await dataStore.set(state, response); - await dataStore.set(`${state}-user-info`, userInfo); - } - - async function waitForResult(state) { - let response; - let attempts = 0; - - while (attempts <= MAX_RETRY_ATTEMPTS) { - response = await dataStore.get(state || ''); - if (response) break; - await delay(RETRY_DELAY_MS); - attempts += 1; - } - - return response; - } - - async function getUserInfo(state) { - return dataStore.get(`${state}-user-info`); - } - - return { saveResult, waitForResult, getUserInfo }; -} - -module.exports = { - ...createAuthResultStore({ dataStore, delay }), - createAuthResultStore, -}; diff --git a/services/authService.js b/services/authService.js deleted file mode 100644 index 56b3c10..0000000 --- a/services/authService.js +++ /dev/null @@ -1,42 +0,0 @@ -const { v4: uuidv4 } = require('uuid'); -const qs = require('qs'); -const { exchangeCodeAndGetUserInfo } = require('../utils/httpClient'); - -function buildAuthorizationUrl({ client, baseUrl, userFlow, server, realm, state, phone }) { - const { client_id: clientId, scope, channel } = client; - const params = { - response_type: 'code', - client_id: clientId, - redirect_uri: `${baseUrl}/auth/callback/${userFlow}/${server.id}`, - scope, - state, - nonce: uuidv4(), - ui_locales: 'en', - consent_id: process.env.CONSENT_ID, - consent_timestamp: Math.floor(Date.now() / 1000), - env: process.env.NODE_ENV || 'development', - }; - - if (channel) params.channel = channel; - if (phone) params.login_hint = phone; - - const endpoint = userFlow === 'pvn_ipificator' - ? 'https://ipificator.ipification.com/api' - : `${server.url}/realms/${realm}/protocol/openid-connect/auth`; - return `${endpoint}?${qs.stringify(params)}`; -} - -async function exchangeAuthorizationCode({ server, realm, clientId, clientSecret, redirectUri, code }) { - const tokenUrl = `${server.url}/realms/${realm}/protocol/openid-connect/token`; - const userUrl = `${server.url}/realms/${realm}/protocol/openid-connect/userinfo`; - const params = { - code, - redirect_uri: redirectUri, - grant_type: 'authorization_code', - client_id: clientId, - client_secret: clientSecret, - }; - return exchangeCodeAndGetUserInfo(tokenUrl, userUrl, params); -} - -module.exports = { buildAuthorizationUrl, exchangeAuthorizationCode }; diff --git a/services/playIntegrityAttemptService.js b/services/playIntegrityAttemptService.js deleted file mode 100644 index 4493177..0000000 --- a/services/playIntegrityAttemptService.js +++ /dev/null @@ -1,267 +0,0 @@ -const crypto = require('node:crypto'); -const jwt = require('jsonwebtoken'); -const { createRequestHash } = require('../utils/canonicalJson'); -const { normalizePhoneNumber } = require('../utils/phoneNumber'); - -const ATTEMPT_TTL_MS = 120_000; -const ACTION = 'IPIFICATION_AUTH'; - -function createPlayIntegrityAttemptService({ - dataStore, - stateSecret, - stateIssuer, - stateAudience, - now = () => new Date(), - randomId = crypto.randomUUID, -}) { - if (!dataStore || typeof dataStore.get !== 'function' || typeof dataStore.set !== 'function') { - throw new TypeError('dataStore must provide get and set methods'); - } - if (typeof stateSecret !== 'string' || stateSecret.length === 0) { - throw new TypeError('stateSecret is required'); - } - if (typeof stateIssuer !== 'string' || stateIssuer.length === 0) { - throw new TypeError('stateIssuer is required'); - } - if (typeof stateAudience !== 'string' || stateAudience.length === 0) { - throw new TypeError('stateAudience is required'); - } - if (typeof randomId !== 'function') throw new TypeError('randomId must be a function'); - - const locks = new Map(); - - function currentTimeMs() { - const value = now(); - const milliseconds = value instanceof Date ? value.getTime() : Number(value); - if (!Number.isFinite(milliseconds)) throw new TypeError('now must return a valid time'); - return milliseconds; - } - - function attemptKey(attemptId) { - return `play-integrity:attempt:${attemptId}`; - } - - function transactionKey(transactionId) { - return `play-integrity:transaction:${transactionId}`; - } - - function withKeyLock(key, operation) { - const previous = locks.get(key) || Promise.resolve(); - let release; - const current = new Promise((resolve) => { release = resolve; }); - const queueTail = previous.then(() => current); - locks.set(key, queueTail); - - return previous.then(operation).finally(() => { - release(); - if (locks.get(key) === queueTail) locks.delete(key); - }); - } - - async function save(key, value) { - const ttl = Date.parse(value.expiresAt) - currentTimeMs(); - if (ttl <= 0) return false; - await dataStore.set(key, freezeSnapshot(value), ttl); - return true; - } - - async function resolveSnapshot(resolver, id, kind) { - if (typeof resolver !== 'function') throw new TypeError(`${kind} resolver must be a function`); - const resolved = await resolver(id); - if (!resolved) throw new Error(`${kind} not found`); - if (resolved.id !== id) throw new Error(`${kind} id does not match requested id`); - return freezeSnapshot(resolved); - } - - async function createAttempt({ phoneNumber, clientId, serverId, resolveClient, resolveServer }) { - if (typeof clientId !== 'string' || clientId.length === 0) throw new TypeError('clientId is required'); - if (typeof serverId !== 'string' || serverId.length === 0) throw new TypeError('serverId is required'); - - const phoneNumberE164 = normalizePhoneNumber(phoneNumber); - const createdAtMs = currentTimeMs(); - const [client, server] = await Promise.all([ - resolveSnapshot(resolveClient, clientId, 'client'), - resolveSnapshot(resolveServer, serverId, 'server'), - ]); - const id = randomId(); - if (typeof id !== 'string' || !/^[\w-]+$/.test(id)) { - throw new TypeError('randomId must return a URL-safe identifier'); - } - const expiresAt = new Date(createdAtMs + ATTEMPT_TTL_MS).toISOString(); - const phoneNumberHash = crypto.createHash('sha256').update(phoneNumberE164, 'utf8').digest('base64url'); - const requestHash = createRequestHash(ACTION, { - attemptId: id, - clientId, - serverId, - phoneNumberHash, - }); - const attempt = freezeSnapshot({ - id, - action: ACTION, - phoneNumber: phoneNumberE164, - phoneNumberHash, - clientId, - serverId, - client, - server, - requestHash, - status: 'created', - createdAt: new Date(createdAtMs).toISOString(), - expiresAt, - }); - - if (!await save(attemptKey(id), attempt)) return null; - return freezeSnapshot({ attemptId: id, requestHash, expiresAt }); - } - - async function claimAttempt(attemptId) { - const key = attemptKey(attemptId); - return withKeyLock(key, async () => { - const attempt = await dataStore.get(key); - if (!attempt || attempt.status !== 'created' || isExpired(attempt, currentTimeMs())) return null; - - const claimed = freezeSnapshot({ ...attempt, status: 'verifying' }); - return await save(key, claimed) ? claimed : null; - }); - } - - async function rejectAttempt(attemptId, reason = 'REJECTED') { - const key = attemptKey(attemptId); - return withKeyLock(key, async () => { - const attempt = await dataStore.get(key); - if (!attempt || attempt.status !== 'verifying' || isExpired(attempt, currentTimeMs())) return null; - - const rejected = freezeSnapshot({ ...attempt, status: 'rejected', reason: String(reason) }); - return await save(key, rejected) ? rejected : null; - }); - } - - async function approveAttempt(attemptId) { - const key = attemptKey(attemptId); - return withKeyLock(key, async () => { - const attempt = await dataStore.get(key); - if (!attempt || attempt.status !== 'verifying' || isExpired(attempt, currentTimeMs())) return null; - - const transactionId = randomId(); - if (typeof transactionId !== 'string' || !/^[\w-]+$/.test(transactionId)) { - throw new TypeError('randomId must return a URL-safe identifier'); - } - const transaction = freezeSnapshot({ - id: transactionId, - attemptId: attempt.id, - attempt: freezeSnapshot(attempt), - status: 'approved', - createdAt: new Date(currentTimeMs()).toISOString(), - expiresAt: attempt.expiresAt, - }); - if (!await save(transactionKey(transactionId), transaction)) return null; - - const approvedAttempt = freezeSnapshot({ ...attempt, status: 'approved' }); - if (!await save(key, approvedAttempt)) return null; - return transaction; - }); - } - - function createSignedState(transaction) { - if (!transaction || transaction.status !== 'approved' || !transaction.attempt) { - throw new TypeError('transaction must be approved'); - } - return jwt.sign( - { - typ: ACTION, - tx: transaction.id, - aid: transaction.attempt.id, - act: transaction.attempt.action, - }, - stateSecret, - { - algorithm: 'HS512', - expiresIn: '120s', - issuer: stateIssuer, - audience: stateAudience, - jwtid: randomId(), - }, - ); - } - - async function claimTransaction(state) { - let claims; - try { - claims = jwt.verify(state, stateSecret, { - algorithms: ['HS512'], - issuer: stateIssuer, - audience: stateAudience, - }); - } catch { - return { status: 'invalid' }; - } - if (!isValidStateClaims(claims)) return { status: 'invalid' }; - - const key = transactionKey(claims.tx); - return withKeyLock(key, async () => { - const transaction = await dataStore.get(key); - if (!transaction || transaction.status !== 'approved' || isExpired(transaction, currentTimeMs())) { - return { status: 'unavailable' }; - } - if (transaction.id !== claims.tx - || transaction.attemptId !== claims.aid - || transaction.attempt?.id !== claims.aid - || transaction.attempt?.action !== claims.act) { - return { status: 'invalid' }; - } - - const exchanging = freezeSnapshot({ ...transaction, status: 'exchanging' }); - return await save(key, exchanging) - ? { status: 'claimed', transaction: exchanging } - : { status: 'unavailable' }; - }); - } - - async function completeTransaction(transactionId, status) { - if (!['consumed', 'failed'].includes(status)) throw new TypeError('transaction status is invalid'); - const key = transactionKey(transactionId); - return withKeyLock(key, async () => { - const transaction = await dataStore.get(key); - if (!transaction || transaction.status !== 'exchanging' || isExpired(transaction, currentTimeMs())) { - return null; - } - const completed = freezeSnapshot({ ...transaction, status }); - return await save(key, completed) ? completed : null; - }); - } - - return { - createAttempt, - claimAttempt, - rejectAttempt, - approveAttempt, - createSignedState, - claimTransaction, - completeTransaction, - }; -} - -function isExpired(value, nowMs) { - return !value.expiresAt || Date.parse(value.expiresAt) <= nowMs; -} - -function isValidStateClaims(claims) { - return claims - && claims.typ === ACTION - && typeof claims.tx === 'string' - && typeof claims.aid === 'string' - && typeof claims.act === 'string'; -} - -function freezeSnapshot(value) { - const clone = structuredClone(value); - return deepFreeze(clone); -} - -function deepFreeze(value) { - if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value; - for (const child of Object.values(value)) deepFreeze(child); - return Object.freeze(value); -} - -module.exports = { createPlayIntegrityAttemptService }; diff --git a/services/playIntegrityService.js b/services/playIntegrityService.js deleted file mode 100644 index d2a9186..0000000 --- a/services/playIntegrityService.js +++ /dev/null @@ -1,112 +0,0 @@ -const axios = require('axios'); -const { GoogleAuth } = require('google-auth-library'); - -const PLAY_INTEGRITY_SCOPE = 'https://www.googleapis.com/auth/playintegrity'; - -class PlayIntegrityUnavailableError extends Error { - constructor(reasonCode, cause) { - super(reasonCode); - this.name = 'PlayIntegrityUnavailableError'; - this.reasonCode = reasonCode; - this.cause = cause; - } -} - -function createPlayIntegrityService({ - packageName, - requireLicensedApp = false, - authClient = new GoogleAuth({ scopes: [PLAY_INTEGRITY_SCOPE] }), - httpClient = axios, - timeoutMs, -}) { - async function verify({ integrityToken, expectedRequestHash, expectedPackageName }) { - const accessToken = await getAccessToken(authClient); - const decoded = await decodeIntegrityToken({ - httpClient, - packageName: expectedPackageName || packageName, - integrityToken, - accessToken, - timeoutMs, - }); - - return evaluateVerdict({ - decoded, - expectedRequestHash, - expectedPackageName: expectedPackageName || packageName, - requireLicensedApp, - }); - } - - return { verify }; -} - -async function getAccessToken(authClient) { - try { - const credential = await authClient.getAccessToken(); - const token = typeof credential === 'string' ? credential : credential?.token; - if (!token) throw new Error('access token unavailable'); - return token; - } catch (error) { - throw new PlayIntegrityUnavailableError('GOOGLE_CREDENTIALS_UNAVAILABLE', error); - } -} - -async function decodeIntegrityToken({ httpClient, packageName, integrityToken, accessToken, timeoutMs }) { - try { - const response = await httpClient.post( - `https://playintegrity.googleapis.com/v1/${packageName}:decodeIntegrityToken`, - { integrity_token: integrityToken }, - { - headers: { Authorization: `Bearer ${accessToken}` }, - timeout: timeoutMs, - }, - ); - return response.data; - } catch (error) { - const reasonCode = ['ECONNABORTED', 'ETIMEDOUT'].includes(error?.code) - ? 'GOOGLE_DECODE_TIMEOUT' - : 'GOOGLE_DECODE_FAILED'; - throw new PlayIntegrityUnavailableError(reasonCode, error); - } -} - -function evaluateVerdict({ - decoded, - expectedRequestHash, - expectedPackageName, - requireLicensedApp, -}) { - const tokenPayload = decoded?.tokenPayloadExternal; - const requestDetails = tokenPayload?.requestDetails; - const requestHash = requestDetails?.requestHash; - const requestPackageName = requestDetails?.requestPackageName; - const appRecognition = tokenPayload?.appIntegrity?.appRecognitionVerdict ?? null; - const deviceIntegrity = Array.isArray(tokenPayload?.deviceIntegrity?.deviceRecognitionVerdict) - ? tokenPayload.deviceIntegrity.deviceRecognitionVerdict - : []; - const appLicensing = tokenPayload?.accountDetails?.appLicensingVerdict ?? null; - const requestHashMatched = requestHash === expectedRequestHash; - const packageNameMatched = requestPackageName === expectedPackageName; - const reasonCodes = []; - - if (!requestHashMatched) reasonCodes.push('REQUEST_HASH_MISMATCH'); - if (!packageNameMatched) reasonCodes.push('PACKAGE_NAME_MISMATCH'); - if (appRecognition !== 'PLAY_RECOGNIZED') reasonCodes.push('APP_NOT_RECOGNIZED'); - if (!deviceIntegrity.includes('MEETS_DEVICE_INTEGRITY')) { - reasonCodes.push('DEVICE_INTEGRITY_NOT_MET'); - } - if (requireLicensedApp && appLicensing !== 'LICENSED') reasonCodes.push('APP_NOT_LICENSED'); - - return { - decision: reasonCodes.length === 0 ? 'allow' : 'deny', - reasonCodes, - requestHashMatched, - verdict: { - appRecognition, - deviceIntegrity, - appLicensing, - }, - }; -} - -module.exports = { createPlayIntegrityService, PlayIntegrityUnavailableError }; diff --git a/services/smsService.js b/services/smsService.js deleted file mode 100644 index dd0378b..0000000 --- a/services/smsService.js +++ /dev/null @@ -1,88 +0,0 @@ -const axios = require('axios'); -const qs = require('qs'); -const { v4: uuidv4 } = require('uuid'); -const logger = require('../utils/logger'); -const { getFormUrlEncodedConfig, getJsonConfig, getUserInfo } = require('../utils/httpClient'); - -function buildCibaAuthFormData(clientId, clientSecret, reqScope, scope, loginHint) { - const formData = { - client_id: clientId, - client_secret: clientSecret, - scope: reqScope || scope || 'openid', - login_hint: loginHint, - channel: 'sms', - message: 'Your verification PIN is: {{code}}' - }; - - return formData; -} - -/** Start SMS authentication and return the provider request identifiers. */ -async function startAuthentication({ authServerUrl, realm, clientId, clientSecret, reqScope, scope, loginHint }) { - const authUrl = `${authServerUrl}/realms/${realm}/protocol/openid-connect/ext/ciba/auth`; - const smsNonce = uuidv4(); - const formData = buildCibaAuthFormData(clientId, clientSecret, reqScope, scope, loginHint); - - logger.log('sms authUrl', authUrl); - logger.log('sms formData', formData); - - const authResponse = await axios.post(authUrl, qs.stringify(formData), getFormUrlEncodedConfig()); - logger.log('SMS auth response:', authResponse.data); - - const authReqId = authResponse.data.auth_req_id; - - if (!authReqId) { - throw new Error('No authReqId received from auth response'); - } - - return { authReqId, nonce: smsNonce }; -} - -async function callCallbackEndpoint(callbackUrl, authReqId, smsCode) { - try { - const callbackPayload = { code: smsCode }; - logger.log('sms callbackUrl', callbackUrl); - logger.log('sms authReqId', authReqId); - logger.log('sms callbackPayload', callbackPayload); - - const callbackResponse = await axios.post(callbackUrl, callbackPayload, { - headers: { - Authorization: `Bearer ${authReqId}`, - ...getJsonConfig().headers - } - }); - - logger.log(`SMS callback response status: ${callbackResponse.status}, data: ${JSON.stringify(callbackResponse.data)}`); - } catch (error) { - // Keep token exchange available even when this callback fails. - logger.error('SMS callback error:', error.message); - } -} - -/** Complete the best-effort callback, then fetch the token and raw userinfo. */ -async function exchangeToken({ authServerUrl, realm, clientId, clientSecret, authReqId, smsCode }) { - const callbackUrl = `${authServerUrl}/realms/${realm}/protocol/openid-connect/ext/bc/sms/callback`; - await callCallbackEndpoint(callbackUrl, authReqId, smsCode); - - const tokenUrl = `${authServerUrl}/realms/${realm}/protocol/openid-connect/token`; - const formData = { - client_id: clientId, - client_secret: clientSecret, - grant_type: 'urn:openid:params:grant-type:ciba', - auth_req_id: authReqId, - }; - - logger.log('sms tokenUrl', tokenUrl); - logger.log('sms formData', formData); - - const authResponse = await axios.post(tokenUrl, qs.stringify(formData), getFormUrlEncodedConfig()); - logger.log('SMS token response:', authResponse.data); - - const { access_token: accessToken } = authResponse.data; - const userUrl = `${authServerUrl}/realms/${realm}/protocol/openid-connect/userinfo`; - const userInfo = await getUserInfo(userUrl, accessToken); - - return userInfo; -} - -module.exports = { startAuthentication, exchangeToken }; diff --git a/services/ts43Service.js b/services/ts43Service.js deleted file mode 100644 index 03916c9..0000000 --- a/services/ts43Service.js +++ /dev/null @@ -1,137 +0,0 @@ -const axios = require('axios'); -const qs = require('qs'); -const { v4: uuidv4 } = require('uuid'); -const logger = require('../utils/logger'); -const { getFormUrlEncodedConfig, getJsonConfig, getUserInfo } = require('../utils/httpClient'); - -function resolveOperation(operation, loginHint) { - if (operation) { - return operation; - } - return loginHint ? "VerifyPhoneNumber" : "GetPhoneNumber"; -} - -function buildCibaAuthFormData(clientId, clientSecret, reqScope, scope, loginHint, carrierHint) { - const formData = { - client_id: clientId, - client_secret: clientSecret, - scope: reqScope || scope || 'openid', - }; - - if (loginHint) { - formData.login_hint = loginHint; - } - - if (carrierHint) { - formData.carrier_hint = carrierHint; - } - - return formData; -} - -function buildDigitalRequest(nonce, dcqlData) { - return { - protocol: "openid4vp-v1-unsigned", - data: { - response_type: "vp_token", - response_mode: "dc_api", - nonce: nonce, - dcql_query: { - credentials: [dcqlData] - } - } - }; -} - -/** Start TS43 authentication and return the provider request identifiers. */ -async function startAuthentication({ authServerUrl, realm, clientId, clientSecret, reqScope, scope, loginHint, carrierHint, operation }) { - // CIBA auth endpoint - const authUrl = `${authServerUrl}/realms/${realm}/protocol/openid-connect/ext/ciba/auth`; - const nonce = uuidv4(); - - const formData = buildCibaAuthFormData(clientId, clientSecret, reqScope, scope, loginHint, carrierHint); - - logger.log('authUrl', authUrl); - logger.log('formData', formData); - - const authResponse = await axios.post(authUrl, qs.stringify(formData), getFormUrlEncodedConfig()); - logger.log('Auth response:', authResponse.data); - - const authReqId = authResponse.data.auth_req_id; - - if (!authReqId) { - throw new Error('No authReqId received from auth response'); - } - - const resolvedOperation = resolveOperation(operation, loginHint); - - // Make the second API call to dcql endpoint - const dcqlUrl = `${authServerUrl}/realms/${realm}/protocol/openid-connect/ext/bc/ts43/dcql`; - const dcqlPayload = { - operation: resolvedOperation, - nonce - }; - - logger.log('dcqlUrl', dcqlUrl); - logger.log('dcqlPayload', dcqlPayload); - - const dcqlResponse = await axios.post(dcqlUrl, dcqlPayload, { - headers: { - 'Authorization': `Bearer ${authReqId}`, - ...getJsonConfig().headers - } - }); - - logger.log('DCQL response:', dcqlResponse.data); - - return { authReqId, nonce, digitalRequest: buildDigitalRequest(nonce, dcqlResponse.data) }; -} - -async function callCallbackEndpoint(callbackUrl, vpToken, authReqId) { - try { - const callbackPayload = { vp_token: vpToken }; - logger.log('callbackUrl', callbackUrl); - logger.log('callbackPayload', callbackPayload); - - const callbackResponse = await axios.post(callbackUrl, callbackPayload, { - headers: { - 'Authorization': `Bearer ${authReqId}`, - ...getJsonConfig().headers - } - }); - - logger.log('Callback response:', callbackResponse.data); - } catch (error) { - // Keep token exchange available even when this callback fails. - logger.error('Callback Error:', error.message); - } -} - -/** Complete the best-effort callback, then fetch the token and raw userinfo. */ -async function exchangeToken({ authServerUrl, realm, clientId, clientSecret, authReqId, vpToken }) { - const callbackUrl = `${authServerUrl}/realms/${realm}/protocol/openid-connect/ext/bc/ts43/callback`; - await callCallbackEndpoint(callbackUrl, vpToken, authReqId); - - const tokenUrl = `${authServerUrl}/realms/${realm}/protocol/openid-connect/token`; - const formData = { - client_id: clientId, - client_secret: clientSecret, - grant_type: "urn:openid:params:grant-type:ciba", - auth_req_id: authReqId, - }; - - logger.log('tokenUrl', tokenUrl); - logger.log('formData', formData); - - const authResponse = await axios.post(tokenUrl, qs.stringify(formData), getFormUrlEncodedConfig()); - logger.log('Token response:', authResponse.data); - - const { access_token: accessToken } = authResponse.data; - const userUrl = `${authServerUrl}/realms/${realm}/protocol/openid-connect/userinfo`; - - const userInfo = await getUserInfo(userUrl, accessToken); - - return userInfo; -} - -module.exports = { startAuthentication, exchangeToken }; diff --git a/socket.js b/socket.js deleted file mode 100644 index c0170b5..0000000 --- a/socket.js +++ /dev/null @@ -1,29 +0,0 @@ -'use strict'; -const debug = require('debug')('socket'); -const { Server } = require("socket.io"); - -const init_socket = (server) => { - const socketIO = new Server(server); - - socketIO.on('connection', (socket) => { - socket.emit("messages", { - event_name: "connected", - socket_id: socket.id - }); - - socket.on("init", (data) => { - if (!data || !data.state) { - debug('[%s] init called without state', socket.id); - return; - } - // join client to room channel - const channel = `auth:${data.state}`; - debug('[%s] joins to %o', socket.id, channel); - socket.join(channel); - }); - }); - - return socketIO; -}; - -module.exports = init_socket diff --git a/test/app/playIntegrity.test.js b/test/app/playIntegrity.test.js deleted file mode 100644 index 6094416..0000000 --- a/test/app/playIntegrity.test.js +++ /dev/null @@ -1,91 +0,0 @@ -const http = require('node:http'); -const test = require('node:test'); -const assert = require('node:assert/strict'); - -function loadFeatureEnabledApp(verify) { - const appPath = require.resolve('../../app'); - const playIntegrityService = require('../../services/playIntegrityService'); - const originalEnabled = process.env.PLAY_INTEGRITY_ENABLED; - const originalStateSecret = process.env.PLAY_INTEGRITY_STATE_SECRET; - const originalStateIssuer = process.env.PLAY_INTEGRITY_STATE_ISSUER; - const originalStateAudience = process.env.PLAY_INTEGRITY_STATE_AUDIENCE; - const originalUserFlow = process.env.PLAY_INTEGRITY_USER_FLOW; - const originalCreateService = playIntegrityService.createPlayIntegrityService; - - process.env.PLAY_INTEGRITY_ENABLED = 'true'; - process.env.PLAY_INTEGRITY_STATE_SECRET = 'test-state-secret'; - process.env.PLAY_INTEGRITY_STATE_ISSUER = 'test-issuer'; - process.env.PLAY_INTEGRITY_STATE_AUDIENCE = 'test-audience'; - process.env.PLAY_INTEGRITY_USER_FLOW = 'pvn_ip'; - playIntegrityService.createPlayIntegrityService = () => ({ verify }); - delete require.cache[appPath]; - - try { - return require('../../app'); - } finally { - playIntegrityService.createPlayIntegrityService = originalCreateService; - if (originalEnabled === undefined) delete process.env.PLAY_INTEGRITY_ENABLED; - else process.env.PLAY_INTEGRITY_ENABLED = originalEnabled; - if (originalStateSecret === undefined) delete process.env.PLAY_INTEGRITY_STATE_SECRET; - else process.env.PLAY_INTEGRITY_STATE_SECRET = originalStateSecret; - if (originalStateIssuer === undefined) delete process.env.PLAY_INTEGRITY_STATE_ISSUER; - else process.env.PLAY_INTEGRITY_STATE_ISSUER = originalStateIssuer; - if (originalStateAudience === undefined) delete process.env.PLAY_INTEGRITY_STATE_AUDIENCE; - else process.env.PLAY_INTEGRITY_STATE_AUDIENCE = originalStateAudience; - if (originalUserFlow === undefined) delete process.env.PLAY_INTEGRITY_USER_FLOW; - else process.env.PLAY_INTEGRITY_USER_FLOW = originalUserFlow; - } -} - -async function withServer(app, run) { - const server = http.createServer(app); - - await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); - const { port } = server.address(); - - try { - await run(`http://127.0.0.1:${port}/api/play-integrity/attempt`); - } finally { - await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); - } -} - -async function assertInvalidRequest(response) { - assert.equal(response.status, 400); - const responseBody = await response.json(); - assert.equal(typeof responseBody.request_id, 'string'); - assert.notEqual(responseBody.request_id, ''); - assert.deepEqual({ ...responseBody, request_id: 'generated-request-id' }, { - request_id: 'generated-request-id', - decision: 'deny', reason_codes: ['INVALID_REQUEST'], - }); -} - -test('mounted Play Integrity endpoint safely rejects malformed and oversized JSON', async () => { - let verifyCalls = 0; - const app = loadFeatureEnabledApp(async () => { - verifyCalls += 1; - return { - decision: 'allow', - reasonCodes: [], - requestHashMatched: true, - verdict: null, - }; - }); - - await withServer(app, async (url) => { - await assertInvalidRequest(await fetch(url, { - method: 'POST', - headers: { 'content-type': 'application/json' }, - body: '{"integrityToken":', - })); - - await assertInvalidRequest(await fetch(url, { - method: 'POST', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ phone_number: '+84901234567', server_id: 'stage', value: 'x'.repeat(17 * 1024) }), - })); - }); - - assert.equal(verifyCalls, 0); -}); diff --git a/test/bootstrap/playIntegrity.test.js b/test/bootstrap/playIntegrity.test.js deleted file mode 100644 index dbdf664..0000000 --- a/test/bootstrap/playIntegrity.test.js +++ /dev/null @@ -1,119 +0,0 @@ -const assert = require('node:assert/strict'); -const http = require('node:http'); -const test = require('node:test'); -const express = require('express'); -const { setupPlayIntegrity } = require('../../bootstrap/playIntegrity'); -const { createAppLocalsMiddleware } = require('../../middleware/appLocals'); -const playIntegrityService = require('../../services/playIntegrityService'); - -function configureEnv(t, enabled = 'true') { - const values = { - PLAY_INTEGRITY_ENABLED: enabled, - PLAY_INTEGRITY_PACKAGE_NAME: 'com.example.setup', - PLAY_INTEGRITY_REQUIRE_LICENSED_APP: 'true', - PLAY_INTEGRITY_STATE_SECRET: 'test-only-setup-secret', - PLAY_INTEGRITY_STATE_ISSUER: 'setup-test', - PLAY_INTEGRITY_STATE_AUDIENCE: 'setup-mobile', - PLAY_INTEGRITY_USER_FLOW: 'pvn_ip', - }; - const previous = Object.fromEntries(Object.keys(values).map(key => [key, process.env[key]])); - t.after(() => { - for (const [key, value] of Object.entries(previous)) { - if (value === undefined) delete process.env[key]; - else process.env[key] = value; - } - }); - for (const [key, value] of Object.entries(values)) { - if (value === undefined) delete process.env[key]; - else process.env[key] = value; - } -} - -const configFixture = () => ({ - realm: 'demo', client_id: 'test-client', client_secret: 'test-client-secret', - clients: [{ user_flow: 'pvn_ip', title: 'IP', scope: 'openid ip:phone_verify' }], - auth_servers: [{ id: 'stage', url: 'https://provider.example/auth' }], - play_integrity_verify_domains: ['127.0.0.1'], -}); - -function setup(config) { - const app = express(); - app.set('trust proxy', 1); - app.use(createAppLocalsMiddleware({ config, locale: {} })); - setupPlayIntegrity(app, { config }); - // Feature parser must see its requests before this general parser. - app.use(express.json()); - app.post('/echo', (req, res) => res.json({ length: req.body.value.length })); - return app; -} - -async function withServer(app, run) { - const server = http.createServer(app); - await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', resolve); }); - const base = `http://127.0.0.1:${server.address().port}`; - const post = (route, body, headers = {}) => fetch(`${base}${route}`, { - method: 'POST', headers: { 'content-type': 'application/json', ...headers }, - body: typeof body === 'string' ? body : JSON.stringify(body), - }); - try { await run(post); } - finally { await new Promise(resolve => server.close(resolve)); } -} - -test('disabled feature mounts no endpoint and does not initialize verification or attempt secrets', async t => { - configureEnv(t, 'false'); - delete process.env.PLAY_INTEGRITY_STATE_SECRET; - t.mock.method(playIntegrityService, 'createPlayIntegrityService', () => { assert.fail('disabled feature initialized Google verification'); }); - await withServer(setup(configFixture()), async post => { - assert.equal((await post('/api/play-integrity/attempt', {})).status, 404); - assert.deepEqual(await (await post('/echo', { value: 'normal' })).json(), { length: 6 }); - }); -}); - -test('enabled feature retains 16kb parsing and safe JSON errors ahead of the global parser', async t => { - configureEnv(t); - t.mock.method(playIntegrityService, 'createPlayIntegrityService', () => ({ verify: async () => { assert.fail('invalid payload reached verification'); } })); - await withServer(setup(configFixture()), async post => { - for (const body of ['{"phone_number":', { value: 'x'.repeat(17 * 1024) }]) { - const response = await post('/api/play-integrity/attempt', body); - assert.equal(response.status, 400); - const result = await response.json(); - assert.equal(typeof result.request_id, 'string'); - assert.ok(result.request_id.length > 0); - assert.deepEqual({ ...result, request_id: 'generated' }, { request_id: 'generated', decision: 'deny', reason_codes: ['INVALID_REQUEST'] }); - } - assert.deepEqual(await (await post('/echo', { value: 'x'.repeat(17 * 1024) })).json(), { length: 17 * 1024 }); - }); -}); - -test('setup passes configured package/licensing/timeout and checks verification domains per request', async t => { - configureEnv(t); - const options = []; - const verification = []; - t.mock.method(playIntegrityService, 'createPlayIntegrityService', opts => { - options.push(opts); - return { verify: async input => { verification.push(input); return { decision: 'allow', reasonCodes: [] }; } }; - }); - await withServer(setup(configFixture()), async post => { - for (const host of ['127.0.0.1', 'bypass.example']) { - const attempt = await post('/api/play-integrity/attempt', { phone_number: '+12025550123', server_id: 'stage' }, { 'x-forwarded-host': host }); - assert.equal(attempt.status, 201); - const body = await attempt.json(); - const response = await post('/api/play-integrity/verify', { attempt_id: body.attempt_id, integrity_token: 'local-test-token' }, { 'x-forwarded-host': host }); - assert.equal(response.status, 201); - const approved = await response.json(); - assert.equal(typeof approved.state, 'string'); - if (host === '127.0.0.1') { - assert.deepEqual(verification, [{ integrityToken: 'local-test-token', expectedRequestHash: body.request_hash, expectedPackageName: 'com.example.setup' }]); - } - } - }); - assert.equal(verification.length, 1); - assert.deepEqual(options, [{ packageName: 'com.example.setup', requireLicensedApp: true, timeoutMs: 10000 }]); -}); - -test('enabled feature still fails startup when attempt state configuration is missing', t => { - configureEnv(t); - delete process.env.PLAY_INTEGRITY_STATE_SECRET; - t.mock.method(playIntegrityService, 'createPlayIntegrityService', () => ({ verify: async () => ({ decision: 'allow' }) })); - assert.throws(() => setup(configFixture()), /stateSecret is required/); -}); diff --git a/test/contracts/fixtures.js b/test/contracts/fixtures.js deleted file mode 100644 index fdb6323..0000000 --- a/test/contracts/fixtures.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - client: { user_flow: 'pvn_ip', client_id: 'fixture-client', client_secret: 'fixture-only-secret', scope: 'openid ip:phone_verify', title: 'Phone' }, - server: { id: 'stage', url: 'https://provider.invalid/auth' }, -}; diff --git a/test/contracts/migrationContract.test.js b/test/contracts/migrationContract.test.js deleted file mode 100644 index 0859a0b..0000000 --- a/test/contracts/migrationContract.test.js +++ /dev/null @@ -1,55 +0,0 @@ -const test = require('node:test'); -const assert = require('node:assert/strict'); -const http = require('node:http'); -const express = require('express'); -const session = require('express-session'); -const { randomUUID } = require('node:crypto'); -const { client, server } = require('./fixtures'); -const store = require('../../lib/data_store'); - -async function fixture(run) { - const app = express(); - app.use(express.json()); - app.use(express.urlencoded({ extended: false })); - app.use(session({ secret: 'test-only', resave: false, saveUninitialized: true })); - app.use((req, res, next) => { - res.locals = { clients: [client], auth_servers: [server], realm: 'fixture', baseUrl: 'https://showcase.example', getAuthServer: id => !id || id === server.id ? server : null }; - next(); - }); - app.use('/', require('../../routes/index')); - app.use('/auth', require('../../routes/auth')); - const listener = http.createServer(app); - await new Promise(resolve => listener.listen(0, '127.0.0.1', resolve)); - try { await run((path, options) => fetch(`http://127.0.0.1:${listener.address().port}${path}`, { redirect: 'manual', ...options })); } - finally { await new Promise(resolve => listener.close(resolve)); } -} - -test('migration contract: config redacts secrets and retains callback without server id', () => fixture(async request => { - const response = await request('/api/config'); - const data = await response.json(); - assert.equal(data.clients[0].redirect_uri, 'https://showcase.example/auth/callback/pvn_ip'); - assert.equal(Object.hasOwn(data.clients[0], 'client_secret'), false); - assert.equal(JSON.stringify(data).includes(client.client_secret), false); -})); -test('migration contract: duplicate server and flow query keys retain lookup failures', () => fixture(async request => { - const serverResponse = await request('/auth/start?user_flow=pvn_ip&state=s&server_id=stage&server_id=stage'); - assert.equal(serverResponse.status, 400); - assert.deepEqual(await serverResponse.json(), { error: 'INVALID_SERVER_ID' }); - const flowResponse = await request('/auth/start?user_flow=pvn_ip&user_flow=pvn_ip&state=s'); - assert.equal(flowResponse.status, 200); - assert.equal(await flowResponse.text(), 'Client not found'); -})); -test('migration contract: S2S accepts JSON and urlencoded without consuming results', () => fixture(async request => { - const state = randomUUID(); - await store.set(`${state}-user-info`, { sub: 'fixture-sub' }); - for (const [contentType, body] of [['application/json', JSON.stringify({state})], ['application/x-www-form-urlencoded', new URLSearchParams({state}).toString()]]) { - const response = await request('/auth/s2s/signin', {method:'POST',headers:{'content-type':contentType},body}); - assert.equal(response.status, 200); - assert.deepEqual(await response.json(), {sub:'fixture-sub'}); - } -})); -test('migration contract: urlencoded mobile body follows invalid-client JSON response', () => fixture(async request => { - const response = await request('/auth/mobile/login', {method:'POST',headers:{'content-type':'application/x-www-form-urlencoded'},body:'client_id=missing'}); - assert.equal(response.status,401); - assert.deepEqual(await response.json(), {error:'Client not found'}); -})); diff --git a/test/middleware/appLocals.test.js b/test/middleware/appLocals.test.js deleted file mode 100644 index df4ea2e..0000000 --- a/test/middleware/appLocals.test.js +++ /dev/null @@ -1,69 +0,0 @@ -const assert = require('node:assert/strict'); -const test = require('node:test'); -const { createAppLocalsMiddleware } = require('../../middleware/appLocals'); - -const configFixture = () => ({ - title: 'Configured title', - client_id: 'shared-client', - client_secret: 'shared-test-secret', - locale: { source: 'config-only' }, - auth_servers: [{ id: 'stage', url: 'https://provider.example' }, { id: 'live', url: 'https://live.example' }], - clients: [ - { user_flow: 'pvn_ip', title: 'IP', client_id: '', client_secret: '' }, - { user_flow: 'pvn_im', title: 'IM', client_id: 'own-client', client_secret: 'own-test-secret' }, - ], -}); - -function invoke(middleware, locals = {}) { - const res = { locals }; - let nextCalls = 0; - middleware({ protocol: 'https', headers: { host: 'showcase.example:8443' } }, res, () => { nextCalls++; }); - assert.equal(nextCalls, 1); - return res.locals; -} - -test('locals preserve earlier values and apply client credential fallbacks without mutating config', () => { - const config = configFixture(); - const locale = { source: 'merged-locale' }; - const middleware = createAppLocalsMiddleware({ config, locale }); - const locals = invoke(middleware, { request_id: 'earlier-middleware', title: 'Earlier title' }); - assert.equal(locals.request_id, 'earlier-middleware'); - assert.equal(locals.title, 'Configured title'); - assert.equal(locals.baseUrl, 'https://showcase.example:8443'); - assert.strictEqual(locals.locale, locale); - assert.deepEqual(locals.clients, [ - { user_flow: 'pvn_ip', title: 'IP', client_id: 'shared-client', client_secret: 'shared-test-secret' }, - { user_flow: 'pvn_im', title: 'IM', client_id: 'own-client', client_secret: 'own-test-secret' }, - ]); - assert.equal(config.clients[0].client_id, ''); - assert.equal(config.clients[0].client_secret, ''); - assert.notStrictEqual(locals.clients[1], config.clients[1]); -}); - -test('locals resolve default/explicit server and flow title with the existing fallbacks', () => { - const config = configFixture(); - const locals = invoke(createAppLocalsMiddleware({ config, locale: {} })); - assert.equal(locals.get_flow_title('pvn_ip', 'Fallback'), 'IP'); - assert.equal(locals.get_flow_title('unknown', 'Fallback'), 'Fallback'); - assert.deepEqual(locals.getAuthServer(), { id: 'stage', url: 'https://provider.example' }); - assert.deepEqual(locals.getAuthServer('live'), { id: 'live', url: 'https://live.example' }); - assert.equal(locals.getAuthServer('unknown'), null); -}); - -test('locals are computed for each request and read the current environment', t => { - const oldEnv = process.env.NODE_ENV; - t.after(() => { if (oldEnv === undefined) delete process.env.NODE_ENV; else process.env.NODE_ENV = oldEnv; }); - const config = configFixture(); - delete config.title; - const middleware = createAppLocalsMiddleware({ config, locale: {} }); - delete process.env.NODE_ENV; - const first = invoke(middleware); - assert.equal(first.app_env, 'development'); - assert.equal(first.title, 'IPification Showcase'); - config.client_id = 'updated-client'; - process.env.NODE_ENV = 'stage'; - const second = invoke(middleware); - assert.equal(second.clients[0].client_id, 'updated-client'); - assert.equal(second.app_env, 'stage'); - assert.equal(first.clients[0].client_id, 'shared-client'); -}); diff --git a/test/routes/authFlows.test.js b/test/routes/authFlows.test.js deleted file mode 100644 index 257f859..0000000 --- a/test/routes/authFlows.test.js +++ /dev/null @@ -1,219 +0,0 @@ -const assert = require('node:assert/strict'); -const http = require('node:http'); -const { randomUUID } = require('node:crypto'); -const test = require('node:test'); -const express = require('express'); -const session = require('express-session'); -const path = require('node:path'); -const authRouter = require('../../routes/auth'); -const dataStore = require('../../lib/data_store'); -const locale = require('../../config/locale.json'); - -const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; - -async function listen(app) { - const server = http.createServer(app); - await new Promise((resolve, reject) => { - server.once('error', reject); - server.listen(0, '127.0.0.1', resolve); - }); - return { server, url: `http://127.0.0.1:${server.address().port}` }; -} - -// Real provider HTTP and real cache; only the desktop socket is a recording stub. -async function withAuth(run, { userInfo = { phone_number: '+12025550123', phone_number_verified: 'true' }, failProvider = false, failSecondWrite = false } = {}) { - const provider = express(); - provider.use(express.urlencoded({ extended: false })); - const requests = []; - provider.post('/realms/demo/protocol/openid-connect/:operation', (req, res) => { - requests.push({ operation: req.params.operation, body: req.body }); - if (failProvider) return res.status(502).json({ error: 'provider-failure' }); - res.json(req.params.operation === 'token' ? { access_token: 'test-access-token' } : userInfo); - }); - const upstream = await listen(provider); - const server = { id: 'stage', url: upstream.url }; - const secondServer = { id: 'live', url: `${upstream.url}/live` }; - const client = { user_flow: 'pvn_ip', client_id: 'demo-client', client_secret: 'test-only-secret', scope: 'openid ip:phone_verify', title: 'Phone', channel: 'wa' }; - const app = express(); - app.set('views', path.join(__dirname, '../../views')); - app.set('view engine', 'pug'); - app.use(express.json()); - app.use(session({ secret: 'test-only-session-secret', resave: false, saveUninitialized: true })); - const events = []; - const order = []; - app.set('socket', { to: room => ({ emit: (event, payload) => { order.push('socket'); events.push({ room, event, payload }); } }) }); - app.use((req, res, next) => { - res.locals = { - clients: [client, { ...client, user_flow: 'pvn_ipificator' }], - getAuthServer: id => !id || id === 'stage' ? server : id === 'live' ? secondServer : null, - realm: 'demo', baseUrl: 'https://showcase.example', auth_servers: [server, secondServer], - locale, get_flow_title: () => '', - }; - next(); - }); - app.get('/test/session', (req, res) => res.json(req.session)); - app.use('/auth', authRouter); - const originalSet = dataStore.set; - dataStore.set = async (key, value, ...args) => { - if (failSecondWrite && key.endsWith('-user-info')) throw new Error('second write failed'); - const result = await originalSet.call(dataStore, key, value, ...args); - order.push(key.endsWith('-user-info') ? 'raw-userinfo' : 'result'); - return result; - }; - let local; - try { - local = await listen(app); - const request = (route, options = {}) => fetch(`${local.url}/auth${route}`, { redirect: 'manual', ...options }); - const post = (route, body) => request(route, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }); - await run({ request, post, events, order, requests, client, server, localUrl: local.url }); - } finally { - dataStore.set = originalSet; - if (local) await new Promise(resolve => local.server.close(resolve)); - await new Promise(resolve => upstream.server.close(resolve)); - } -} - -test('start constructs authorization fields with default and explicit server selection', async () => { - await withAuth(async ({ request, server }) => { - const before = Math.floor(Date.now() / 1000); - const response = await request('/start?user_flow=pvn_ip&state=original-state&phone=%2B12025550123'); - assert.equal(response.status, 302); - const url = new URL(response.headers.get('location')); - assert.equal(url.origin, server.url); - assert.equal(url.pathname, '/realms/demo/protocol/openid-connect/auth'); - const fields = Object.fromEntries(url.searchParams); - assert.deepEqual({ ...fields, nonce: undefined, consent_timestamp: undefined, consent_id: undefined, env: undefined }, { - response_type: 'code', client_id: 'demo-client', redirect_uri: 'https://showcase.example/auth/callback/pvn_ip/stage', - scope: 'openid ip:phone_verify', state: 'original-state', nonce: undefined, ui_locales: 'en', - consent_id: undefined, consent_timestamp: undefined, env: undefined, channel: 'wa', login_hint: '+12025550123', - }); - assert.match(fields.nonce, uuidPattern); - assert.equal(fields.env, process.env.NODE_ENV || 'development'); - assert.equal(fields.consent_id, process.env.CONSENT_ID); - assert.ok(Number(fields.consent_timestamp) >= before && Number(fields.consent_timestamp) <= Math.floor(Date.now() / 1000)); - const explicit = new URL((await request('/start?user_flow=pvn_ip&state=s&server_id=live')).headers.get('location')); - assert.equal(explicit.pathname, '/live/realms/demo/protocol/openid-connect/auth'); - assert.equal(explicit.searchParams.get('redirect_uri'), 'https://showcase.example/auth/callback/pvn_ip/live'); - assert.equal(explicit.searchParams.has('login_hint'), false); - }); -}); - -test('start retains IPificator URL and invalid client/server/state responses', async () => { - await withAuth(async ({ request }) => { - const special = new URL((await request('/start?user_flow=pvn_ipificator&state=s')).headers.get('location')); - assert.equal(special.origin + special.pathname, 'https://ipificator.ipification.com/api'); - const invalidServer = await request('/start?server_id=unknown'); - assert.equal(invalidServer.status, 400); - assert.deepEqual(await invalidServer.json(), { error: 'INVALID_SERVER_ID' }); - const invalidClient = await request('/start?user_flow=unknown&state=s'); - assert.equal(invalidClient.status, 200); - assert.equal(await invalidClient.text(), 'Client not found'); - assert.equal((await request('/start?user_flow=pvn_ip')).headers.get('location'), '/auth/login'); - }); -}); - -test('web callback exchanges code, stores formatted and raw results, then completes session', async () => { - await withAuth(async ({ request, post, requests, localUrl, order }) => { - const state = randomUUID(); - const callback = await request(`/callback/pvn_ip/stage?code=test-code&state=${state}`); - assert.equal(callback.status, 302); - assert.equal(callback.headers.get('location'), `https://showcase.example/auth/complete?state=${state}`); - assert.deepEqual(requests, [ - { operation: 'token', body: { code: 'test-code', redirect_uri: 'https://showcase.example/auth/callback/pvn_ip/stage', grant_type: 'authorization_code', client_id: 'demo-client', client_secret: 'test-only-secret' } }, - { operation: 'userinfo', body: { access_token: 'test-access-token' } }, - ]); - assert.deepEqual(order, ['result', 'raw-userinfo']); - const stored = await dataStore.get(state); - assert.equal(stored.state, state); - assert.equal(stored.client_id, 'demo-client'); - assert.equal(stored.client_title, 'Phone'); - assert.match(stored.userInfo, /<.*phone_number/); - const complete = await request(`/complete?state=${state}`); - assert.equal(complete.headers.get('location'), '/user/info'); - const cookie = complete.headers.get('set-cookie').split(';', 1)[0]; - const sessionData = await (await fetch(`${localUrl}/test/session`, { headers: { cookie } })).json(); - assert.equal(sessionData.isAuthenticated, true); - assert.deepEqual(sessionData.userData, stored); - assert.deepEqual(await (await post('/s2s/signin', { state })).json(), { phone_number: '+12025550123', phone_number_verified: 'true' }); - }); -}); - -for (const [verified, destination] of [['true', 'complete'], ['false', 'error'], [false, 'complete']]) { - test(`QR callback preserves ${JSON.stringify(verified)} verification behavior and write/notify order`, async () => { - await withAuth(async ({ request, events, order }) => { - const state = `${randomUUID()}-qrcode-middle`; - const response = await request(`/callback/pvn_ip/stage?code=c&state=${state}`); - assert.equal(response.headers.get('location'), `/auth/qrcode/${destination}`); - assert.deepEqual(order, ['result', 'raw-userinfo', 'socket']); - assert.deepEqual(events, [{ room: `auth:${state}`, event: 'messages', payload: { event_name: 'url', url: `https://showcase.example/auth/complete?state=${state}` } }]); - assert.equal((await request(`/qrcode/${destination}`)).status, 200); - }, { userInfo: { phone_number_verified: verified } }); - }); -} - -test('backchannel takes precedence over QR, stores results and responds with empty 200', async () => { - await withAuth(async ({ request, post, events, order }) => { - const state = `${randomUUID()}-qrcode`; - const response = await request(`/callback/pvn_ip/stage?code=c&state=${state}`, { headers: { 'ip-backchannel-im-auth': 'true' } }); - assert.equal(response.status, 200); - assert.equal(await response.text(), ''); - assert.deepEqual(events, []); - assert.deepEqual(order, ['result', 'raw-userinfo']); - assert.equal((await post('/s2s/signin', { state })).status, 200); - for (const body of [{ state: randomUUID() }, {}]) { - const missing = await post('/s2s/signin', body); - assert.equal(missing.status, 401); - assert.equal(await missing.text(), ''); - } - }); -}); - -test('failed second cache write prevents QR notification and success redirect', async () => { - await withAuth(async ({ request, events, order }) => { - const response = await request(`/callback/pvn_ip/stage?code=c&state=${randomUUID()}-qrcode`); - assert.equal(response.status, 400); - assert.equal(await response.text(), 'second write failed'); - assert.equal(response.headers.get('location'), null); - assert.deepEqual(events, []); - assert.deepEqual(order, ['result']); - }, { failSecondWrite: true }); -}); - -test('callback generates a UUID when state is absent and retains lookup errors', async () => { - await withAuth(async ({ request }) => { - const response = await request('/callback/pvn_ip/stage?code=c'); - const state = new URL(response.headers.get('location')).searchParams.get('state'); - assert.match(state, uuidPattern); - assert.equal((await dataStore.get(state)).state, state); - const invalidServer = await request('/callback/pvn_ip/unknown'); - assert.equal(invalidServer.status, 400); - assert.equal(await invalidServer.text(), "Invalid server_id: 'unknown'"); - assert.equal(await (await request('/callback/unknown/stage')).text(), 'Client not found'); - }); -}); - -test('callback and mobile preserve their distinct provider failure responses', async () => { - await withAuth(async ({ request, post }) => { - const callback = await request(`/callback/pvn_ip/stage?code=c&state=${randomUUID()}`); - assert.equal(callback.status, 400); - assert.equal(await callback.text(), 'Request failed with status code 502'); - const mobile = await post('/mobile/login', { client_id: 'demo-client', code: 'c', redirect_uri: 'demo://callback' }); - assert.equal(mobile.status, 401); - assert.deepEqual(await mobile.json(), { error: 'Request failed with status code 502' }); - }, { failProvider: true }); -}); - -test('mobile exchanges request redirect URI, returns userinfo and preserves merge precedence', async () => { - await withAuth(async ({ post, requests }) => { - const response = await post('/mobile/login', { client_id: 'demo-client', code: 'mobile-code', redirect_uri: 'demo://callback', server_id: 'stage' }); - assert.equal(response.status, 200); - assert.deepEqual(await response.json(), { auth_server: 'provider-value', phone_number: '+12025550123' }); - assert.deepEqual(requests[0].body, { code: 'mobile-code', redirect_uri: 'demo://callback', grant_type: 'authorization_code', client_id: 'demo-client', client_secret: 'test-only-secret' }); - const invalidServer = await post('/mobile/login', { server_id: 'unknown' }); - assert.equal(invalidServer.status, 400); - assert.deepEqual(await invalidServer.json(), { error: "Invalid server_id: 'unknown'" }); - const invalidClient = await post('/mobile/login', { client_id: 'unknown' }); - assert.equal(invalidClient.status, 401); - assert.deepEqual(await invalidClient.json(), { error: 'Client not found' }); - }, { userInfo: { auth_server: 'provider-value', phone_number: '+12025550123' } }); -}); diff --git a/test/routes/authSecurity.test.js b/test/routes/authSecurity.test.js deleted file mode 100644 index d0a0ad0..0000000 --- a/test/routes/authSecurity.test.js +++ /dev/null @@ -1,69 +0,0 @@ -const assert = require('node:assert/strict'); -const http = require('node:http'); -const path = require('node:path'); -const test = require('node:test'); -const express = require('express'); -const session = require('express-session'); - -const authRouter = require('../../routes/auth'); -const locale = require('../../config/locale.json'); - -async function withAuthApp(handler) { - const app = express(); - app.set('views', path.join(__dirname, '../../views')); - app.set('view engine', 'pug'); - app.use(session({ secret: 'test-session-secret', resave: false, saveUninitialized: true })); - app.use((req, res, next) => { - res.locals = { - clients: [], - auth_servers: [], - getAuthServer: () => null, - get_flow_title: () => '', - locale, - realm: 'ipification', - baseUrl: 'http://127.0.0.1', - }; - next(); - }); - app.use('/auth', authRouter); - - const server = http.createServer(app); - await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); - try { - await handler(`http://127.0.0.1:${server.address().port}`); - } finally { - await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); - } -} - -test('rejects an invalid server_id without reflecting it in the response', async () => { - const payload = ''; - - await withAuthApp(async (baseUrl) => { - const response = await fetch(`${baseUrl}/auth/start?server_id=${encodeURIComponent(payload)}`); - - assert.equal(response.status, 400); - assert.match(response.headers.get('content-type'), /^application\/json/); - assert.deepEqual(await response.json(), { error: 'INVALID_SERVER_ID' }); - }); -}); - -test('maps callback errors to a fixed message without rendering attacker-controlled details', async () => { - await withAuthApp(async (baseUrl) => { - const callbackResponse = await fetch( - `${baseUrl}/auth/callback/pvn_ip/Indonesia?error=operator_not_resolved&error_description=${encodeURIComponent('Press Windows+R and run this command')}&state=attacker-state`, - { redirect: 'manual' }, - ); - const cookie = callbackResponse.headers.get('set-cookie').split(';', 1)[0]; - - assert.equal(callbackResponse.status, 302); - assert.equal(callbackResponse.headers.get('location'), '/auth/login'); - - const loginResponse = await fetch(`${baseUrl}/auth/login`, { headers: { cookie } }); - const loginPage = await loginResponse.text(); - - assert.match(loginPage, /Your mobile operator could not be resolved\./); - assert.doesNotMatch(loginPage, /Press Windows\+R and run this command/); - assert.doesNotMatch(loginPage, /attacker-state/); - }); -}); diff --git a/test/routes/cibaFlows.test.js b/test/routes/cibaFlows.test.js deleted file mode 100644 index acb3aca..0000000 --- a/test/routes/cibaFlows.test.js +++ /dev/null @@ -1,217 +0,0 @@ -const assert = require('node:assert/strict'); -const http = require('node:http'); -const test = require('node:test'); -const express = require('express'); -const session = require('express-session'); - -const protocols = ['ts43', 'sms']; -const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; - -async function listen(app) { - const server = http.createServer(app); - await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', resolve); }); - return { server, url: `http://127.0.0.1:${server.address().port}` }; -} - -// All protocol calls hit a local provider, including the best-effort callback. -async function withProvider(protocol, run, options = {}) { - const provider = express(); - provider.use(express.urlencoded({ extended: false })); - provider.use(express.json()); - const requests = []; - provider.post(/.*/, (req, res) => { - const operation = req.path.endsWith('/ciba/auth') ? 'auth' - : req.path.endsWith('/dcql') ? 'dcql' - : req.path.endsWith('/callback') ? 'callback' - : req.path.endsWith('/token') ? 'token' : 'userinfo'; - requests.push({ operation, path: req.path, body: req.body, authorization: req.headers.authorization, contentType: req.headers['content-type'] }); - if (options.failAt === operation) return res.status(503).json({ error: 'provider-unavailable' }); - const results = { - auth: options.missingAuthReqId ? {} : { auth_req_id: 'provider-auth-id' }, - dcql: { id: 'phone', format: 'ts43' }, callback: { accepted: true }, - token: { access_token: 'provider-token' }, - userinfo: options.userInfo || { phone_number: '+12025550123', phone_number_verified: true }, - }; - res.json(results[operation]); - }); - const upstream = await listen(provider); - try { await run({ server: { id: 'stage', url: upstream.url }, requests }); } - finally { await new Promise(resolve => upstream.server.close(resolve)); } -} - -async function withRoutes(protocol, run, options = {}) { - await withProvider(protocol, async ({ server, requests }) => { - const app = express(); - app.use(express.json()); - app.use(session({ secret: 'test-only-ciba-session', resave: false, saveUninitialized: true })); - const client = { client_id: 'test-client', client_secret: 'test-only-secret', scope: options.clientScope === undefined ? 'openid ip:phone' : options.clientScope, title: options.clientTitle === undefined ? 'Client title' : options.clientTitle }; - app.use((req, res, next) => { - res.locals = { clients: [client], realm: 'demo', getAuthServer: id => !id || id === 'stage' ? server : null }; - next(); - }); - app.get('/test/session', (req, res) => res.json(req.session)); - app.use(`/${protocol}`, require(`../../routes/${protocol}`)); - const local = await listen(app); - const post = (endpoint, body) => fetch(`${local.url}/${protocol}${endpoint}`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }); - const sessionFor = async response => { - const cookie = response.headers.get('set-cookie').split(';', 1)[0]; - return (await fetch(`${local.url}/test/session`, { headers: { cookie } })).json(); - }; - try { await run({ post, sessionFor, server, requests, client }); } - finally { await new Promise(resolve => local.server.close(resolve)); } - }, options); -} - -for (const protocol of protocols) { - test(`${protocol} auth preserves form fields, nonce, and protocol-specific response`, async () => { - await withRoutes(protocol, async ({ post, requests, server }) => { - const response = await post('/auth', { client_id: 'test-client', server_id: 'stage', login_hint: '+12025550123', carrier_hint: 'carrier', scope: 'requested-scope' }); - assert.equal(response.status, 200); - const body = await response.json(); - assert.deepEqual(body.auth_server, server); - assert.equal(body.auth_req_id, 'provider-auth-id'); - assert.match(body.nonce, uuidPattern); - assert.equal(requests[0].path, '/realms/demo/protocol/openid-connect/ext/ciba/auth'); - const expectedForm = { client_id: 'test-client', client_secret: 'test-only-secret', scope: 'requested-scope', login_hint: '+12025550123' }; - if (protocol === 'sms') Object.assign(expectedForm, { channel: 'sms', message: 'Your verification PIN is: {{code}}' }); - else expectedForm.carrier_hint = 'carrier'; - assert.deepEqual(requests[0].body, expectedForm); - assert.match(requests[0].contentType, /^application\/x-www-form-urlencoded/); - if (protocol === 'ts43') { - assert.deepEqual(requests[1].body, { operation: 'VerifyPhoneNumber', nonce: body.nonce }); - assert.equal(requests[1].authorization, 'Bearer provider-auth-id'); - assert.deepEqual(body.digital_request, { protocol: 'openid4vp-v1-unsigned', data: { response_type: 'vp_token', response_mode: 'dc_api', nonce: body.nonce, dcql_query: { credentials: [{ id: 'phone', format: 'ts43' }] } } }); - } else { - assert.equal(requests.length, 1); - assert.deepEqual(Object.keys(body).sort(), ['auth_req_id', 'auth_server', 'nonce']); - } - }); - }); - - for (const [clientScope, expectedScope] of [['configured-scope', 'configured-scope'], ['', 'openid']]) { - test(`${protocol} auth retains scope fallback to ${expectedScope}`, async () => { - await withRoutes(protocol, async ({ post, requests }) => { - assert.equal((await post('/auth', { client_id: 'test-client' })).status, 200); - assert.equal(requests[0].body.scope, expectedScope); - assert.equal(Object.hasOwn(requests[0].body, 'login_hint'), false); - if (protocol === 'ts43') { - assert.equal(requests[1].body.operation, 'GetPhoneNumber'); - assert.equal(Object.hasOwn(requests[0].body, 'carrier_hint'), false); - } - }, { clientScope }); - }); - } - - for (const options of [{ failAt: 'auth' }, { missingAuthReqId: true }, ...(protocol === 'ts43' ? [{ failAt: 'dcql' }] : [])]) { - test(`${protocol} auth preserves provider error payload for ${JSON.stringify(options)}`, async () => { - await withRoutes(protocol, async ({ post, requests, server }) => { - const response = await post('/auth', { client_id: 'test-client' }); - const status = options.missingAuthReqId ? 500 : 503; - assert.equal(response.status, status); - assert.deepEqual(await response.json(), { - auth_server: server, success: false, - error: options.missingAuthReqId ? 'No authReqId received from auth response' : 'Request failed with status code 503', - status, ...(options.missingAuthReqId ? {} : { data: { error: 'provider-unavailable' } }), - }); - assert.equal(requests.length, options.failAt === 'dcql' ? 2 : 1); - }, options); - }); - } - - test(`${protocol} keeps endpoint-specific invalid server/client responses and log acknowledgement`, async () => { - await withRoutes(protocol, async ({ post, requests }) => { - for (const endpoint of ['/auth', '/token']) { - const server = await post(endpoint, { server_id: 'unknown' }); - assert.equal(server.status, 400); - assert.deepEqual(await server.json(), { error: "Invalid server_id: 'unknown'" }); - const client = await post(endpoint, { client_id: 'unknown' }); - assert.equal(client.status, 401); - if (endpoint === '/auth') assert.equal(await client.text(), 'Client not found'); - else assert.deepEqual(await client.json(), { error: 'Client not found' }); - } - assert.equal(await (await post('/log', { data: { note: 'local-test' } })).text(), 'OK'); - assert.deepEqual(requests, []); - }); - }); - - for (const failAt of [undefined, 'callback']) { - test(`${protocol} token completes callback, token, userinfo, session even when callback ${failAt ? 'fails' : 'succeeds'}`, async () => { - await withRoutes(protocol, async ({ post, requests, sessionFor, server }) => { - const response = await post('/token', { client_id: 'test-client', auth_req_id: 'provider-auth-id', nonce: 'browser-nonce', vp_token: 'presentation', code: '123456' }); - assert.equal(response.status, 200); - assert.deepEqual(await response.json(), { auth_server: server, phone_number: '+12025550123', phone_number_verified: true }); - assert.deepEqual(requests.map(req => req.operation), ['callback', 'token', 'userinfo']); - assert.equal(requests[0].path, `/realms/demo/protocol/openid-connect/ext/bc/${protocol}/callback`); - assert.equal(requests[0].authorization, 'Bearer provider-auth-id'); - assert.deepEqual(requests[0].body, protocol === 'ts43' ? { vp_token: 'presentation' } : { code: '123456' }); - assert.deepEqual(requests[1].body, { client_id: 'test-client', client_secret: 'test-only-secret', grant_type: 'urn:openid:params:grant-type:ciba', auth_req_id: 'provider-auth-id' }); - assert.deepEqual(requests[2].body, { access_token: 'provider-token' }); - const stored = await sessionFor(response); - assert.equal(stored.isAuthenticated, true); - assert.equal(stored.userData.client_id, 'test-client'); - assert.equal(stored.userData.client_title, protocol === 'ts43' ? 'SIM' : 'Client title'); - assert.equal(stored.userData.state, 'browser-nonce'); - assert.match(stored.userData.userInfo, /phone_number/); - }, { failAt }); - }); - } - - for (const failAt of ['token', 'userinfo']) { - test(`${protocol} token ${failAt} error preserves status, provider data and error session`, async () => { - await withRoutes(protocol, async ({ post, requests, sessionFor }) => { - const response = await post('/token', { client_id: 'test-client', auth_req_id: 'provider-auth-id', nonce: 'browser-nonce', vp_token: 'presentation', code: '123456' }); - assert.equal(response.status, 503); - assert.deepEqual(await response.json(), { error: 'Request failed with status code 503', status: 503, data: { error: 'provider-unavailable' } }); - assert.deepEqual(requests.map(req => req.operation), failAt === 'token' ? ['callback', 'token'] : ['callback', 'token', 'userinfo']); - const stored = await sessionFor(response); - assert.equal(stored.isAuthenticated, true); - assert.equal(stored.userData.client_title, protocol === 'ts43' ? 'SIM' : 'Client title'); - assert.equal(stored.userData.state, 'browser-nonce'); - assert.match(stored.userData.userInfo, /provider-unavailable/); - }, { failAt }); - }); - } - - test(`${protocol} token preserves userinfo spread precedence and SMS title fallback`, async () => { - await withRoutes(protocol, async ({ post, sessionFor }) => { - const response = await post('/token', { client_id: 'test-client', nonce: 'browser-nonce' }); - assert.deepEqual(await response.json(), { auth_server: 'provider-value' }); - assert.equal((await sessionFor(response)).userData.client_title, protocol === 'ts43' ? 'SIM' : 'SMS'); - }, { userInfo: { auth_server: 'provider-value' }, clientTitle: '' }); - }); -} - -test('TS43 retains an explicit operation instead of deriving it from login hint', async () => { - await withRoutes('ts43', async ({ post, requests }) => { - assert.equal((await post('/auth', { client_id: 'test-client', operation: 'CustomOperation', login_hint: '+12025550123' })).status, 200); - assert.equal(requests[1].body.operation, 'CustomOperation'); - }); -}); - -// These contracts also verify services can run without Express request/response objects. -for (const protocol of protocols) { - test(`${protocol} service starts authentication without HTTP handler dependencies`, async () => { - const service = require(`../../services/${protocol}Service`); - await withProvider(protocol, async ({ server, requests }) => { - const result = await service.startAuthentication({ authServerUrl: server.url, realm: 'demo', clientId: 'test-client', clientSecret: 'test-only-secret', reqScope: 'requested-scope', scope: 'client-scope', loginHint: '+12025550123' }); - assert.equal(result.authReqId, 'provider-auth-id'); - assert.match(result.nonce, uuidPattern); - assert.equal(requests[0].body.scope, 'requested-scope'); - if (protocol === 'ts43') { - assert.equal(result.digitalRequest.data.nonce, result.nonce); - assert.equal(requests[1].body.operation, 'VerifyPhoneNumber'); - } else { - assert.equal(requests[0].body.channel, 'sms'); - } - }); - }); - - test(`${protocol} service returns raw userinfo after callback failure without writing a session`, async () => { - const service = require(`../../services/${protocol}Service`); - await withProvider(protocol, async ({ server, requests }) => { - const result = await service.exchangeToken({ authServerUrl: server.url, realm: 'demo', clientId: 'test-client', clientSecret: 'test-only-secret', authReqId: 'provider-auth-id', vpToken: 'presentation', smsCode: '123456' }); - assert.deepEqual(result, { phone_number: '+12025550123', phone_number_verified: true }); - assert.deepEqual(requests.map(req => req.operation), ['callback', 'token', 'userinfo']); - }, { failAt: 'callback' }); - }); -} diff --git a/test/routes/playIntegrityAttempt.test.js b/test/routes/playIntegrityAttempt.test.js deleted file mode 100644 index 566a17e..0000000 --- a/test/routes/playIntegrityAttempt.test.js +++ /dev/null @@ -1,464 +0,0 @@ -const http = require('node:http'); -const test = require('node:test'); -const assert = require('node:assert/strict'); -const express = require('express'); -const jwt = require('jsonwebtoken'); -const { PlayIntegrityUnavailableError } = require('../../services/playIntegrityService'); -const { createPlayIntegrityAttemptService } = require('../../services/playIntegrityAttemptService'); -const { createPlayIntegrityRouter } = require('../../routes/playIntegrity'); - -const publicAttempt = { - attemptId: 'attempt-id', - requestHash: 'expected-request-hash', - expiresAt: '2026-09-10T10:02:00.000Z', -}; - -const frozenAttempt = Object.freeze({ - id: publicAttempt.attemptId, - action: 'IPIFICATION_AUTH', - requestHash: publicAttempt.requestHash, - expiresAt: publicAttempt.expiresAt, - clientId: 'demo', - serverId: 'stage', - client: Object.freeze({ - id: 'demo', - client_id: 'demo', - client_secret: 'snapshot-client-secret', - redirectUri: 'https://client.example/callback', - }), - server: Object.freeze({ - id: 'stage', - url: 'https://server.example/auth', - realm: 'ipification', - }), -}); - -function createAttemptService(overrides = {}) { - return { - createAttempt: async () => publicAttempt, - claimAttempt: async () => frozenAttempt, - rejectAttempt: async () => frozenAttempt, - approveAttempt: async () => ({ - id: 'transaction-id', - attempt: frozenAttempt, - status: 'approved', - expiresAt: publicAttempt.expiresAt, - }), - createSignedState: () => 'signed.state.value', - claimTransaction: async () => ({ status: 'claimed', transaction: { - id: 'transaction-id', attempt: frozenAttempt, status: 'exchanging', - } }), - completeTransaction: async () => undefined, - ...overrides, - }; -} - -function createRealAttemptService() { - const values = new Map(); - return createPlayIntegrityAttemptService({ - dataStore: { - get: async (key) => values.get(key), - set: async (key, value) => values.set(key, value), - }, - stateSecret: 'trusted-state-secret', - stateIssuer: 'ipification-demo', - stateAudience: 'ipification-sdk', - }); -} - -function createRouter({ attemptService, verify, exchangeCodeAndGetUserInfo, logCompletion, bypassVerification, verifyRequiredForRequest, logVerificationFailure } = {}) { - return createPlayIntegrityRouter({ - attemptService: attemptService || createAttemptService(), - verify: verify || (async () => ({ decision: 'allow', reasonCodes: [] })), - exchangeCodeAndGetUserInfo: exchangeCodeAndGetUserInfo || (async () => ({ userInfo: {} })), - expectedPackageName: 'com.example.demo', - userFlow: 'mobile', - bypassVerification, - verifyRequiredForRequest, - requestIdFactory: () => 'request-id', - logCompletion, - logVerificationFailure, - }); -} - -async function withServer(router, run) { - const app = express(); - app.use((req, res, next) => { - res.locals = { - clients: [{ client_id: 'demo', client_secret: 'runtime-client-secret', user_flow: 'mobile' }], - getAuthServer: (serverId) => serverId === 'stage' - ? { id: 'stage', url: 'https://runtime.example/auth' } - : null, - realm: 'runtime-realm', - baseUrl: 'https://showcase.example', - }; - next(); - }); - app.use('/api/play-integrity', router); - const server = http.createServer(app); - - await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); - const { port } = server.address(); - const post = async (path, body) => fetch(`http://127.0.0.1:${port}/api/play-integrity${path}`, { - method: 'POST', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify(body), - }); - - try { - await run(post); - } finally { - await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); - } -} - -test('attempt rejects a missing serverId and never creates storage', async () => { - let createCalls = 0; - const router = createRouter({ - attemptService: createAttemptService({ createAttempt: async () => { createCalls += 1; } }), - }); - - await withServer(router, async (post) => { - const response = await post('/attempt', { phone_number: '+84901234567' }); - assert.equal(response.status, 400); - assert.deepEqual(await response.json(), { - request_id: 'request-id', decision: 'deny', reason_codes: ['INVALID_REQUEST'], - }); - }); - - assert.equal(createCalls, 0); -}); - -test('attempt resolves its configured user flow without accepting client_id', async () => { - let received; - const router = createRouter({ - attemptService: createAttemptService({ - createAttempt: async (input) => { - received = input; - return publicAttempt; - }, - }), - }); - - await withServer(router, async (post) => { - const response = await post('/attempt', { - phone_number: '+84901234567', server_id: 'stage', - }); - assert.equal(response.status, 201); - assert.deepEqual(await response.json(), { - attempt_id: publicAttempt.attemptId, - request_hash: publicAttempt.requestHash, - expires_at: publicAttempt.expiresAt, - }); - }); - - assert.equal(received.phoneNumber, '+84901234567'); - assert.equal(received.clientId, 'demo'); - assert.equal(received.serverId, 'stage'); - assert.equal(received.resolveClient().user_flow, 'mobile'); -}); - -test('verify issues state only after a matching verdict', async () => { - let verifyInput; - const events = []; - const router = createRouter({ - logCompletion: (event) => events.push(event), - verify: async (input) => { - verifyInput = input; - return { decision: 'allow', reasonCodes: [] }; - }, - }); - - await withServer(router, async (post) => { - const response = await post('/verify', { attempt_id: publicAttempt.attemptId, integrity_token: 'opaque' }); - assert.equal(response.status, 201); - assert.deepEqual(await response.json(), { - state: 'signed.state.value', expires_at: publicAttempt.expiresAt, - }); - }); - - assert.deepEqual(verifyInput, { - integrityToken: 'opaque', - expectedRequestHash: publicAttempt.requestHash, - expectedPackageName: 'com.example.demo', - }); - assert.equal(events[0].verification_mode, 'verify'); -}); - -test('verify bypass issues state without calling Google verification when the request domain is not configured', async () => { - let verifyCalls = 0; - const events = []; - const router = createRouter({ - logCompletion: (event) => events.push(event), - verifyRequiredForRequest: () => false, - verify: async () => { verifyCalls += 1; throw new Error('Google must not be called'); }, - }); - - await withServer(router, async (post) => { - const response = await post('/verify', { attempt_id: publicAttempt.attemptId, integrity_token: 'opaque' }); - assert.equal(response.status, 201); - assert.deepEqual(await response.json(), { - state: 'signed.state.value', expires_at: publicAttempt.expiresAt, - }); - }); - - assert.equal(verifyCalls, 0); - assert.equal(events[0].verification_mode, 'bypass'); -}); - -test('verify logs the safe upstream error details without the integrity token', async () => { - const events = []; - const router = createRouter({ - verify: async () => { throw new PlayIntegrityUnavailableError('GOOGLE_DECODE_FAILED', new Error('upstream unavailable: opaque-secret-token')); }, - logVerificationFailure: (event) => events.push(event), - }); - - await withServer(router, async (post) => { - const response = await post('/verify', { attempt_id: publicAttempt.attemptId, integrity_token: 'opaque-secret-token' }); - assert.equal(response.status, 503); - }); - - assert.deepEqual(events, [{ - request_id: 'request-id', - reason_code: 'GOOGLE_DECODE_FAILED', - error_name: 'Error', - error_message: 'upstream unavailable: [REDACTED]', - }]); - assert.equal(JSON.stringify(events).includes('opaque-secret-token'), false); -}); - -test('verify denies a hash mismatch without issuing state', async () => { - let rejectedReason; - let approved = 0; - const router = createRouter({ - attemptService: createAttemptService({ - rejectAttempt: async (_, reason) => { rejectedReason = reason; }, - approveAttempt: async () => { approved += 1; }, - }), - verify: async () => ({ decision: 'deny', reasonCodes: ['REQUEST_HASH_MISMATCH'] }), - }); - - await withServer(router, async (post) => { - const response = await post('/verify', { attempt_id: publicAttempt.attemptId, integrity_token: 'opaque' }); - assert.equal(response.status, 403); - const body = await response.json(); - assert.equal(Object.hasOwn(body, 'state'), false); - assert.deepEqual(body, { - request_id: 'request-id', decision: 'deny', reason_codes: ['REQUEST_HASH_MISMATCH'], - }); - }); - - assert.equal(rejectedReason, 'REQUEST_HASH_MISMATCH'); - assert.equal(approved, 0); -}); - -test('verify logs a clear reason for every policy reason code', async () => { - const events = []; - const router = createRouter({ - logCompletion: (event) => events.push(event), - verify: async () => ({ - decision: 'deny', - reasonCodes: ['REQUEST_HASH_MISMATCH', 'DEVICE_INTEGRITY_NOT_MET'], - }), - }); - - await withServer(router, async (post) => { - const response = await post('/verify', { attempt_id: publicAttempt.attemptId, integrity_token: 'opaque' }); - assert.equal(response.status, 403); - }); - - assert.deepEqual(events[0], { - requestId: 'request-id', - endpoint: 'verify', - verification_mode: 'verify', - decision: 'deny', - reasonCodes: ['REQUEST_HASH_MISMATCH', 'DEVICE_INTEGRITY_NOT_MET'], - reasonDetails: [ - 'Decoded request hash does not match the server-stored request hash', - 'Device integrity verdict does not include MEETS_DEVICE_INTEGRITY', - ], - duration: events[0].duration, - }); -}); - -test('verify rejects an expired or claimed attempt before Google is called', async () => { - let verifyCalls = 0; - const router = createRouter({ - attemptService: createAttemptService({ claimAttempt: async () => null }), - verify: async () => { verifyCalls += 1; }, - }); - - await withServer(router, async (post) => { - const response = await post('/verify', { attempt_id: publicAttempt.attemptId, integrity_token: 'opaque' }); - assert.equal(response.status, 409); - assert.deepEqual(await response.json(), { - request_id: 'request-id', decision: 'deny', reason_codes: ['ATTEMPT_UNAVAILABLE'], - }); - }); - - assert.equal(verifyCalls, 0); -}); - -test('verify maps unavailable Google verification without exposing its input', async () => { - const router = createRouter({ - verify: async () => { - throw new PlayIntegrityUnavailableError('GOOGLE_DECODE_FAILED', new Error('opaque')); - }, - }); - - await withServer(router, async (post) => { - const response = await post('/verify', { attempt_id: publicAttempt.attemptId, integrity_token: 'opaque' }); - assert.equal(response.status, 503); - assert.deepEqual(await response.json(), { - request_id: 'request-id', decision: 'unavailable', reason_codes: ['GOOGLE_DECODE_FAILED'], - }); - }); -}); - -test('token exchange rejects invalid state', async () => { - let exchangeCalls = 0; - const router = createRouter({ - exchangeCodeAndGetUserInfo: async () => { exchangeCalls += 1; }, - }); - - await withServer(router, async (post) => { - const response = await post('/token-exchange', { code: 'input', state: '' }); - assert.equal(response.status, 400); - assert.deepEqual(await response.json(), { - request_id: 'request-id', decision: 'deny', reason_codes: ['INVALID_REQUEST'], - }); - }); - - assert.equal(exchangeCalls, 0); -}); - -test('token exchange rejects a forged compact JWT as invalid state', async () => { - let exchangeCalls = 0; - const router = createRouter({ - attemptService: createRealAttemptService(), - exchangeCodeAndGetUserInfo: async () => { exchangeCalls += 1; }, - }); - const forgedState = jwt.sign( - { typ: 'IPIFICATION_AUTH', tx: 'forged-transaction', aid: 'forged-attempt', act: 'IPIFICATION_AUTH' }, - 'untrusted-state-secret', - { algorithm: 'HS512', issuer: 'ipification-demo', audience: 'ipification-sdk' }, - ); - - await withServer(router, async (post) => { - const response = await post('/token-exchange', { code: 'input', state: forgedState }); - assert.equal(response.status, 400); - assert.deepEqual(await response.json(), { - request_id: 'request-id', decision: 'deny', reason_codes: ['INVALID_STATE'], - }); - }); - - assert.equal(exchangeCalls, 0); -}); - -test('token exchange rejects reuse before a second IPification call', async () => { - let claims = 0; - const exchangeInputs = []; - const completions = []; - const router = createRouter({ - attemptService: createAttemptService({ - claimTransaction: async () => (claims++ === 0 ? { status: 'claimed', transaction: { - id: 'transaction-id', attempt: frozenAttempt, status: 'exchanging', - } } : { status: 'unavailable' }), - completeTransaction: async (...input) => completions.push(input), - }), - exchangeCodeAndGetUserInfo: async (...input) => { - exchangeInputs.push(input); - return { userInfo: { subject: 'not-returned' } }; - }, - }); - - await withServer(router, async (post) => { - const first = await post('/token-exchange', { code: 'input', state: 'signed.state.value' }); - assert.equal(first.status, 200); - assert.deepEqual(await first.json(), { - decision: 'allow', - user_info: { subject: 'not-returned' }, - }); - - const reused = await post('/token-exchange', { code: 'different-input', state: 'signed.state.value' }); - assert.equal(reused.status, 409); - assert.deepEqual(await reused.json(), { - request_id: 'request-id', decision: 'deny', reason_codes: ['TRANSACTION_UNAVAILABLE'], - }); - }); - - assert.deepEqual(exchangeInputs, [[ - 'https://server.example/auth/realms/ipification/protocol/openid-connect/token', - 'https://server.example/auth/realms/ipification/protocol/openid-connect/userinfo', - { - code: 'input', - redirect_uri: 'https://client.example/callback', - grant_type: 'authorization_code', - client_id: 'demo', - client_secret: 'snapshot-client-secret', - }, - ]]); - assert.deepEqual(completions, [['transaction-id', 'consumed']]); -}); - -test('token exchange returns user info from the IPification exchange', async () => { - const userInfo = { - sub: 'user-123', - phone_number: '+84901234567', - }; - const router = createRouter({ - exchangeCodeAndGetUserInfo: async () => ({ - tokenInfo: { access_token: 'must-not-be-returned' }, - userInfo, - }), - }); - - await withServer(router, async (post) => { - const response = await post('/token-exchange', { code: 'input', state: 'signed.state.value' }); - assert.equal(response.status, 200); - assert.deepEqual(await response.json(), { - decision: 'allow', - user_info: userInfo, - }); - }); -}); - -test('token exchange records a failed transaction without exposing the dependency error', async () => { - const completions = []; - const router = createRouter({ - attemptService: createAttemptService({ - completeTransaction: async (...input) => completions.push(input), - }), - exchangeCodeAndGetUserInfo: async () => { throw new Error('dependency details'); }, - }); - - await withServer(router, async (post) => { - const response = await post('/token-exchange', { code: 'input', state: 'signed.state.value' }); - assert.equal(response.status, 401); - assert.deepEqual(await response.json(), { - request_id: 'request-id', decision: 'deny', reason_codes: ['IPIFICATION_EXCHANGE_FAILED'], - }); - }); - - assert.deepEqual(completions, [['transaction-id', 'failed']]); -}); - -test('responses and completion logs omit sensitive request values', async () => { - const events = []; - const router = createRouter({ logCompletion: (event) => events.push(event) }); - - await withServer(router, async (post) => { - const response = await post('/attempt', { - phone_number: '+84901234567', server_id: 'stage', - }); - assert.equal(response.status, 201); - const body = JSON.stringify(await response.json()); - assert.doesNotMatch(body, /\+84901234567|runtime-client-secret|snapshot-client-secret/); - }); - - assert.deepEqual(events, [{ - requestId: 'request-id', endpoint: 'attempt', decision: 'allow', reasonCodes: [], duration: events[0].duration, - }]); - assert.equal(typeof events[0].duration, 'number'); - assert.doesNotMatch(JSON.stringify(events), /\+84901234567|runtime-client-secret|snapshot-client-secret/); -}); diff --git a/test/routes/ts43RateLimit.test.js b/test/routes/ts43RateLimit.test.js deleted file mode 100644 index 612e4cb..0000000 --- a/test/routes/ts43RateLimit.test.js +++ /dev/null @@ -1,50 +0,0 @@ -const assert = require('node:assert/strict'); -const http = require('node:http'); -const test = require('node:test'); -const express = require('express'); - -const ts43Router = require('../../routes/ts43'); -const { createTs43AuthRateLimiter } = require('../../middleware/ts43RateLimit'); - -async function withTs43App(handler) { - const app = express(); - app.use(express.json()); - app.use((req, res, next) => { - res.locals = { - clients: [], - getAuthServer: () => null, - realm: 'ipification', - }; - next(); - }); - app.use('/ts43/auth', createTs43AuthRateLimiter({ maxRequests: 2, windowMs: 60_000 })); - app.use('/ts43', ts43Router); - - const server = http.createServer(app); - await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); - try { - await handler(`http://127.0.0.1:${server.address().port}`); - } finally { - await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); - } -} - -test('returns 429 with Retry-After when TS43 authentication exceeds its rate limit', async () => { - await withTs43App(async (baseUrl) => { - const request = () => fetch(`${baseUrl}/ts43/auth`, { - method: 'POST', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ client_id: 'demo-client', login_hint: '+84901234567' }), - }); - - const first = await request(); - assert.equal(first.status, 400); - assert.equal(first.headers.get('ratelimit-policy'), '2;w=60'); - assert.equal((await request()).status, 400); - - const limited = await request(); - assert.equal(limited.status, 429); - assert.equal(limited.headers.get('retry-after'), '60'); - assert.deepEqual(await limited.json(), { error: 'RATE_LIMITED' }); - }); -}); diff --git a/test/services/authResultStore.test.js b/test/services/authResultStore.test.js deleted file mode 100644 index fcfc73e..0000000 --- a/test/services/authResultStore.test.js +++ /dev/null @@ -1,71 +0,0 @@ -const assert = require('node:assert/strict'); -const test = require('node:test'); -const { createAuthResultStore } = require('../../services/authResultStore'); - -function fixture({ failWrite = false, hitOnRead } = {}) { - const values = new Map(); - const operations = []; - let reads = 0; - const store = createAuthResultStore({ - dataStore: { - async set(key, value) { - operations.push(['set', key, value]); - if (failWrite) throw new Error('write failed'); - values.set(key, value); - }, - async get(key) { - operations.push(['get', key]); - reads++; - if (reads === hitOnRead) values.set(key, { state: key }); - return values.get(key); - }, - }, - delay: async ms => { operations.push(['delay', ms]); }, - }); - return { store, values, operations }; -} - -test('stores formatted result then raw userinfo without changing or consuming them', async () => { - const { store, values, operations } = fixture(); - await store.saveResult('s', { userInfo: '
phone
' }, { phone: '+12025550123' }); - assert.deepEqual(operations, [ - ['set', 's', { userInfo: '
phone
' }], - ['set', 's-user-info', { phone: '+12025550123' }], - ]); - assert.deepEqual(values.get('s'), { userInfo: '
phone
' }); - assert.deepEqual(await store.getUserInfo('s'), { phone: '+12025550123' }); - assert.deepEqual(await store.getUserInfo('s'), { phone: '+12025550123' }); -}); - -test('does not write userinfo after the first result write fails', async () => { - const { store, operations } = fixture({ failWrite: true }); - await assert.rejects(store.saveResult('s', { state: 's' }, {}), /write failed/); - assert.deepEqual(operations, [['set', 's', { state: 's' }]]); -}); - -test('returns an immediate result without sleeping', async () => { - const { store, values, operations } = fixture(); - values.set('s', { state: 's' }); - assert.deepEqual(await store.waitForResult('s'), { state: 's' }); - assert.deepEqual(operations, [['get', 's']]); -}); - -test('returns a delayed result after exactly two sleeps when read three succeeds', async () => { - const { store, operations } = fixture({ hitOnRead: 3 }); - assert.deepEqual(await store.waitForResult('s'), { state: 's' }); - assert.deepEqual(operations, [['get', 's'], ['delay', 1000], ['get', 's'], ['delay', 1000], ['get', 's']]); -}); - -for (const state of ['missing', undefined]) { - test(`preserves five reads and five sleeps for missing result (${state})`, async () => { - const { store, operations } = fixture(); - assert.equal(await store.waitForResult(state), undefined); - assert.deepEqual(operations, Array.from({ length: 5 }, () => [['get', state || ''], ['delay', 1000]]).flat()); - }); -} - -test('propagates cache read errors without sleeping or retrying', async () => { - const store = createAuthResultStore({ dataStore: { get: async () => { throw new Error('read failed'); } }, delay: async () => { assert.fail('unexpected delay'); } }); - await assert.rejects(store.waitForResult('s'), /read failed/); - await assert.rejects(store.getUserInfo('s'), /read failed/); -}); diff --git a/test/services/authService.test.js b/test/services/authService.test.js deleted file mode 100644 index 90eb775..0000000 --- a/test/services/authService.test.js +++ /dev/null @@ -1,55 +0,0 @@ -const assert = require('node:assert/strict'); -const http = require('node:http'); -const test = require('node:test'); -const express = require('express'); -const { buildAuthorizationUrl, exchangeAuthorizationCode } = require('../../services/authService'); - -const authorization = { - client: { client_id: 'demo', scope: 'openid ip:phone', channel: 'wa' }, - baseUrl: 'https://showcase.example', userFlow: 'pvn_ip', - server: { id: 'stage', url: 'https://provider.example/auth' }, realm: 'demo', state: 'original-state', phone: '+12025550123', -}; - -test('authorization service builds the provider URL and all request fields', () => { - const url = new URL(buildAuthorizationUrl(authorization)); - assert.equal(url.origin + url.pathname, 'https://provider.example/auth/realms/demo/protocol/openid-connect/auth'); - assert.equal(url.searchParams.get('redirect_uri'), 'https://showcase.example/auth/callback/pvn_ip/stage'); - assert.equal(url.searchParams.get('response_type'), 'code'); - assert.equal(url.searchParams.get('client_id'), 'demo'); - assert.equal(url.searchParams.get('scope'), 'openid ip:phone'); - assert.equal(url.searchParams.get('state'), 'original-state'); - assert.equal(url.searchParams.get('channel'), 'wa'); - assert.equal(url.searchParams.get('login_hint'), '+12025550123'); - assert.equal(url.searchParams.get('ui_locales'), 'en'); -}); - -test('authorization service preserves IPificator endpoint and omits absent optional fields', () => { - const url = new URL(buildAuthorizationUrl({ ...authorization, userFlow: 'pvn_ipificator', client: { client_id: 'demo', scope: 'openid' }, phone: undefined })); - assert.equal(url.origin + url.pathname, 'https://ipificator.ipification.com/api'); - assert.equal(url.searchParams.get('redirect_uri'), 'https://showcase.example/auth/callback/pvn_ipificator/stage'); - assert.equal(url.searchParams.has('login_hint'), false); - assert.equal(url.searchParams.has('channel'), false); -}); - -test('code exchange uses explicit client and redirect parameters and returns token plus userinfo', async () => { - const app = express(); - app.use(express.urlencoded({ extended: false })); - const requests = []; - app.post('/realms/demo/protocol/openid-connect/:operation', (req, res) => { - requests.push({ operation: req.params.operation, body: req.body, contentType: req.headers['content-type'] }); - res.json(req.params.operation === 'token' ? { access_token: 'local-token', expires_in: 60 } : { sub: 'demo-user' }); - }); - const server = http.createServer(app); - await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', resolve); }); - try { - const result = await exchangeAuthorizationCode({ - server: { url: `http://127.0.0.1:${server.address().port}` }, realm: 'demo', clientId: 'request-client', clientSecret: 'test-only-secret', redirectUri: 'demo://callback', code: 'test-code', - }); - assert.deepEqual(result, { tokenInfo: { access_token: 'local-token', expires_in: 60 }, userInfo: { sub: 'demo-user' } }); - assert.deepEqual(requests.map(({ operation, body }) => ({ operation, body })), [ - { operation: 'token', body: { code: 'test-code', redirect_uri: 'demo://callback', grant_type: 'authorization_code', client_id: 'request-client', client_secret: 'test-only-secret' } }, - { operation: 'userinfo', body: { access_token: 'local-token' } }, - ]); - for (const req of requests) assert.match(req.contentType, /^application\/x-www-form-urlencoded/); - } finally { await new Promise(resolve => server.close(resolve)); } -}); diff --git a/test/services/playIntegrityAttemptService.test.js b/test/services/playIntegrityAttemptService.test.js deleted file mode 100644 index 5548660..0000000 --- a/test/services/playIntegrityAttemptService.test.js +++ /dev/null @@ -1,264 +0,0 @@ -const test = require('node:test'); -const assert = require('node:assert/strict'); -const jwt = require('jsonwebtoken'); -const { - createPlayIntegrityAttemptService, -} = require('../../services/playIntegrityAttemptService'); - -const stateSecret = 'test-state-secret-that-is-long-enough'; -const fixedNow = new Date('2026-09-10T10:00:00.000Z'); - -function createDataStore() { - const values = new Map(); - const writes = []; - return { - values, - writes, - async get(key) { - return values.get(key); - }, - async set(key, value, ttl) { - writes.push({ key, value, ttl }); - values.set(key, value); - }, - }; -} - -function createService({ randomIds = ['attempt-1', 'transaction-1', 'jwt-1'] } = {}) { - const dataStore = createDataStore(); - let idIndex = 0; - return { - dataStore, - service: createPlayIntegrityAttemptService({ - dataStore, - stateSecret, - stateIssuer: 'ipification-demo', - stateAudience: 'ipification-sdk', - now: () => fixedNow, - randomId: () => randomIds[idIndex++], - }), - }; -} - -async function createAttempt(service, overrides = {}) { - return service.createAttempt({ - phoneNumber: '+84 901-234-567', - clientId: 'demo', - serverId: 'stage', - resolveClient: (id) => ({ id, redirectUri: 'https://client.example/callback' }), - resolveServer: (id) => ({ id, tokenEndpoint: 'https://server.example/token' }), - ...overrides, - }); -} - -async function approvedTransaction(service) { - const attempt = await createAttempt(service); - assert.ok(await service.claimAttempt(attempt.attemptId)); - const transaction = await service.approveAttempt(attempt.attemptId); - assert.ok(transaction); - return transaction; -} - -test('creates an immutable E.164 attempt and returns only public data', async () => { - const { service, dataStore } = createService(); - const client = { id: 'demo', redirectUri: 'https://client.example/callback' }; - const result = await createAttempt(service, { resolveClient: () => client }); - - assert.match(result.attemptId, /^[\w-]+$/); - assert.match(result.requestHash, /^[A-Za-z0-9_-]{43}$/); - assert.equal(Object.hasOwn(result, 'phoneNumber'), false); - assert.equal(Object.hasOwn(result, 'client'), false); - assert.equal(dataStore.writes[0].key, 'play-integrity:attempt:attempt-1'); - assert.equal(dataStore.writes[0].ttl, 120_000); - - client.redirectUri = 'https://attacker.example/callback'; - const stored = await dataStore.get('play-integrity:attempt:attempt-1'); - assert.equal(stored.phoneNumber, '+84901234567'); - assert.equal(stored.client.redirectUri, 'https://client.example/callback'); - assert.equal(Object.isFrozen(stored), true); - assert.equal(Object.isFrozen(stored.client), true); -}); - -test('binds the request hash to the immutable attempt snapshot', async () => { - const { service } = createService({ randomIds: ['attempt-1', 'attempt-2'] }); - const first = await createAttempt(service, { phoneNumber: '+84901234567' }); - const second = await createAttempt(service, { phoneNumber: '+84901234568' }); - - assert.notEqual(first.requestHash, second.requestHash); -}); - -test('rejects phone numbers that cannot be normalized to E.164', async () => { - const { service } = createService(); - - await assert.rejects( - createAttempt(service, { phoneNumber: '0901234567' }), - /E\.164/, - ); -}); - -test('requires both client and server resolvers', async () => { - const { service } = createService(); - - await assert.rejects( - service.createAttempt({ - phoneNumber: '+84901234567', clientId: 'demo', serverId: 'stage', resolveServer: () => ({ id: 'stage' }), - }), - /client resolver/, - ); - await assert.rejects( - service.createAttempt({ - phoneNumber: '+84901234567', clientId: 'demo', serverId: 'stage', resolveClient: () => ({ id: 'demo' }), - }), - /server resolver/, - ); -}); - -test('rejects unresolved client or server IDs', async () => { - const { service } = createService(); - - await assert.rejects( - createAttempt(service, { resolveClient: () => null }), - /client not found/, - ); - await assert.rejects( - createAttempt(service, { resolveServer: () => null }), - /server not found/, - ); -}); - -test('rejects resolved snapshots whose IDs do not match the requested IDs', async () => { - const { service } = createService(); - - await assert.rejects( - createAttempt(service, { resolveClient: () => ({ id: 'other-client' }) }), - /client id does not match/, - ); - await assert.rejects( - createAttempt(service, { resolveServer: () => ({ id: 'other-server' }) }), - /server id does not match/, - ); -}); - -test('fails closed when resolver time exhausts the attempt TTL before persistence', async () => { - const dataStore = createDataStore(); - let time = fixedNow.getTime(); - const service = createPlayIntegrityAttemptService({ - dataStore, - stateSecret, - stateIssuer: 'ipification-demo', - stateAudience: 'ipification-sdk', - now: () => time, - randomId: () => 'attempt-1', - }); - - const result = await createAttempt(service, { - resolveClient: async () => { - time += 120_000; - return { id: 'demo' }; - }, - }); - - assert.equal(result, null); - assert.equal(dataStore.writes.length, 0); -}); - -test('allows exactly one concurrent attempt claim', async () => { - const { service } = createService(); - const { attemptId } = await createAttempt(service); - - const [first, second] = await Promise.all([ - service.claimAttempt(attemptId), - service.claimAttempt(attemptId), - ]); - - assert.equal([first, second].filter(Boolean).length, 1); - assert.equal((await service.claimAttempt(attemptId)), null); -}); - -test('rejects only a claimed attempt and preserves the rejection reason', async () => { - const { service, dataStore } = createService(); - const { attemptId } = await createAttempt(service); - - assert.equal(await service.rejectAttempt(attemptId, 'POLICY_REJECTED'), null); - assert.ok(await service.claimAttempt(attemptId)); - const rejected = await service.rejectAttempt(attemptId, 'POLICY_REJECTED'); - - assert.equal(rejected.status, 'rejected'); - assert.equal(rejected.reason, 'POLICY_REJECTED'); - assert.equal((await dataStore.get(`play-integrity:attempt:${attemptId}`)).status, 'rejected'); -}); - -test('approves a claimed attempt by storing the transaction before finalizing the attempt', async () => { - const { service, dataStore } = createService(); - const { attemptId } = await createAttempt(service); - await service.claimAttempt(attemptId); - - const transaction = await service.approveAttempt(attemptId); - const transactionWrite = dataStore.writes.findIndex( - (write) => write.key === 'play-integrity:transaction:transaction-1', - ); - const approvalWrite = dataStore.writes.findIndex( - (write) => write.key === `play-integrity:attempt:${attemptId}` && write.value.status === 'approved', - ); - - assert.equal(transaction.status, 'approved'); - assert.equal(transaction.attempt.phoneNumber, '+84901234567'); - assert.equal(Object.isFrozen(transaction.attempt), true); - assert.equal(dataStore.writes[transactionWrite].ttl, 120_000); - assert.ok(transactionWrite < approvalWrite); -}); - -test('rejects a signed state after its transaction has been claimed once', async () => { - const { service } = createService(); - const transaction = await approvedTransaction(service); - const state = service.createSignedState(transaction); - - assert.equal((await service.claimTransaction(state)).status, 'claimed'); - assert.equal((await service.claimTransaction(state)).status, 'unavailable'); -}); - -test('does not claim a state whose signed attempt or action does not match the transaction', async () => { - const { service } = createService(); - const transaction = await approvedTransaction(service); - const mismatchedState = jwt.sign( - { typ: 'IPIFICATION_AUTH', tx: transaction.id, aid: 'other-attempt', act: transaction.attempt.action }, - stateSecret, - { - algorithm: 'HS512', - expiresIn: '120s', - issuer: 'ipification-demo', - audience: 'ipification-sdk', - jwtid: 'other-jwt', - }, - ); - - assert.equal((await service.claimTransaction(mismatchedState)).status, 'invalid'); - assert.equal(transaction.status, 'approved'); -}); - -test('completes only an exchanging transaction as consumed or failed', async () => { - const { service, dataStore } = createService(); - const transaction = await approvedTransaction(service); - const state = service.createSignedState(transaction); - - assert.equal((await service.claimTransaction(state)).status, 'claimed'); - const consumed = await service.completeTransaction(transaction.id, 'consumed'); - assert.equal(consumed.status, 'consumed'); - assert.equal((await dataStore.get(`play-integrity:transaction:${transaction.id}`)).status, 'consumed'); - assert.equal(await service.completeTransaction(transaction.id, 'failed'), null); -}); - -test('does not claim state with a wrong issuer, audience, or signature', async () => { - const { service } = createService(); - const transaction = await approvedTransaction(service); - const state = service.createSignedState(transaction); - const wrongAudience = jwt.sign( - { typ: 'IPIFICATION_AUTH', tx: transaction.id, aid: transaction.attempt.id, act: transaction.attempt.action }, - stateSecret, - { algorithm: 'HS512', expiresIn: '120s', issuer: 'ipification-demo', audience: 'other', jwtid: 'jwt-other' }, - ); - - assert.equal((await service.claimTransaction(wrongAudience)).status, 'invalid'); - assert.equal((await service.claimTransaction(`${state}x`)).status, 'invalid'); - assert.equal((await service.claimTransaction(state)).status, 'claimed'); -}); diff --git a/test/services/playIntegrityService.test.js b/test/services/playIntegrityService.test.js deleted file mode 100644 index e94abcf..0000000 --- a/test/services/playIntegrityService.test.js +++ /dev/null @@ -1,225 +0,0 @@ -const test = require('node:test'); -const assert = require('node:assert/strict'); -const { - createPlayIntegrityService, - PlayIntegrityUnavailableError, -} = require('../../services/playIntegrityService'); - -const validInput = { - integrityToken: 'opaque-integrity-token', - expectedRequestHash: 'expected-request-hash', - expectedPackageName: 'com.example.demo', -}; - -const fixedNow = new Date('2026-09-10T10:00:00.000Z'); - -function decodedFixture(overrides = {}) { - return { - tokenPayloadExternal: { - requestDetails: { - requestHash: validInput.expectedRequestHash, - requestPackageName: validInput.expectedPackageName, - timestampMillis: String(fixedNow.getTime()), - }, - appIntegrity: { appRecognitionVerdict: 'PLAY_RECOGNIZED' }, - deviceIntegrity: { deviceRecognitionVerdict: ['MEETS_DEVICE_INTEGRITY'] }, - accountDetails: { appLicensingVerdict: 'LICENSED' }, - ...overrides, - }, - }; -} - -function createService({ decoded = decodedFixture(), authError, httpError, requireLicensedApp = false } = {}) { - const authClient = { - async getAccessToken() { - if (authError) throw authError; - return { token: 'access-token' }; - }, - }; - const httpClient = { - async post() { - if (httpError) throw httpError; - return { data: decoded }; - }, - }; - return { - service: createPlayIntegrityService({ - packageName: 'com.example.demo', - requireLicensedApp, - authClient, - httpClient, - timeoutMs: 10_000, - now: () => fixedNow, - }), - httpClient, - }; -} - -test('allows a recognized app with matching hash and device integrity', async () => { - const { service } = createService(); - - const result = await service.verify(validInput); - - assert.deepEqual(result, { - decision: 'allow', - reasonCodes: [], - requestHashMatched: true, - verdict: { - appRecognition: 'PLAY_RECOGNIZED', - deviceIntegrity: ['MEETS_DEVICE_INTEGRITY'], - appLicensing: 'LICENSED', - }, - }); -}); - -test('denies a mismatched package but ignores token timestamp freshness', async () => { - const { service: mismatchedPackage } = createService({ - decoded: decodedFixture({ requestDetails: { - requestHash: validInput.expectedRequestHash, - requestPackageName: 'com.other.demo', - timestampMillis: String(fixedNow.getTime()), - } }), - }); - const { service: stale } = createService({ - decoded: decodedFixture({ requestDetails: { - requestHash: validInput.expectedRequestHash, - requestPackageName: validInput.expectedPackageName, - timestampMillis: String(fixedNow.getTime() - 120_001), - } }), - }); - - assert.deepEqual((await mismatchedPackage.verify(validInput)).reasonCodes, ['PACKAGE_NAME_MISMATCH']); - assert.deepEqual((await stale.verify(validInput)).reasonCodes, []); - assert.equal((await stale.verify(validInput)).decision, 'allow'); -}); - -test('denies a mismatched request hash before other policy failures', async () => { - const { service } = createService({ - decoded: decodedFixture({ - requestDetails: { - requestHash: 'wrong-hash', - requestPackageName: validInput.expectedPackageName, - timestampMillis: String(fixedNow.getTime()), - }, - appIntegrity: { appRecognitionVerdict: 'UNRECOGNIZED_VERSION' }, - deviceIntegrity: { deviceRecognitionVerdict: [] }, - accountDetails: { appLicensingVerdict: 'UNLICENSED' }, - }), - requireLicensedApp: true, - }); - - const result = await service.verify(validInput); - - assert.deepEqual(result.reasonCodes, [ - 'REQUEST_HASH_MISMATCH', - 'APP_NOT_RECOGNIZED', - 'DEVICE_INTEGRITY_NOT_MET', - 'APP_NOT_LICENSED', - ]); - assert.equal(result.decision, 'deny'); - assert.equal(result.requestHashMatched, false); -}); - -test('denies missing app or device verdicts', async () => { - const { service } = createService({ - decoded: decodedFixture({ - appIntegrity: {}, - deviceIntegrity: {}, - }), - }); - - const result = await service.verify(validInput); - - assert.equal(result.decision, 'deny'); - assert.deepEqual(result.reasonCodes, [ - 'APP_NOT_RECOGNIZED', - 'DEVICE_INTEGRITY_NOT_MET', - ]); - assert.deepEqual(result.verdict, { - appRecognition: null, - deviceIntegrity: [], - appLicensing: 'LICENSED', - }); -}); - -test('denies an unlicensed app only when strict licensing is enabled', async () => { - const { service } = createService({ - decoded: decodedFixture({ accountDetails: { appLicensingVerdict: 'UNLICENSED' } }), - requireLicensedApp: true, - }); - - const result = await service.verify(validInput); - - assert.equal(result.decision, 'deny'); - assert.deepEqual(result.reasonCodes, ['APP_NOT_LICENSED']); -}); - -test('does not expose decoded payloads or access tokens in normal results', async () => { - const { service } = createService(); - - const serialized = JSON.stringify(await service.verify(validInput)); - - assert.doesNotMatch(serialized, /access-token/); - assert.doesNotMatch(serialized, /tokenPayloadExternal/); -}); - -test('does not write decoded Play Integrity verdicts to stdout', async () => { - const { service } = createService(); - const originalLog = console.log; - let logCalls = 0; - console.log = () => { logCalls += 1; }; - - try { - await service.verify(validInput); - } finally { - console.log = originalLog; - } - - assert.equal(logCalls, 0); -}); - -test('maps authentication failures to unavailable credential errors', async () => { - const { service } = createService({ authError: new Error('credential details') }); - - await assert.rejects( - service.verify(validInput), - (error) => error instanceof PlayIntegrityUnavailableError - && error.reasonCode === 'GOOGLE_CREDENTIALS_UNAVAILABLE' - && !JSON.stringify(error).includes('credential details'), - ); -}); - -test('maps decode network failures to unavailable decode errors', async () => { - const { service } = createService({ httpError: new Error('network response body') }); - - await assert.rejects( - service.verify(validInput), - (error) => error instanceof PlayIntegrityUnavailableError - && error.reasonCode === 'GOOGLE_DECODE_FAILED' - && !JSON.stringify(error).includes('network response body'), - ); -}); - -test('maps decode timeout failures to unavailable timeout errors', async () => { - const timeoutError = new Error('request timed out'); - timeoutError.code = 'ECONNABORTED'; - const { service } = createService({ httpError: timeoutError }); - - await assert.rejects( - service.verify(validInput), - (error) => error instanceof PlayIntegrityUnavailableError - && error.reasonCode === 'GOOGLE_DECODE_TIMEOUT', - ); -}); - -test('maps ETIMEDOUT decode failures to unavailable timeout errors', async () => { - const timeoutError = new Error('request timed out'); - timeoutError.code = 'ETIMEDOUT'; - const { service } = createService({ httpError: timeoutError }); - - await assert.rejects( - service.verify(validInput), - (error) => error instanceof PlayIntegrityUnavailableError - && error.reasonCode === 'GOOGLE_DECODE_TIMEOUT', - ); -}); diff --git a/test/utils/canonicalJson.test.js b/test/utils/canonicalJson.test.js deleted file mode 100644 index fcbce96..0000000 --- a/test/utils/canonicalJson.test.js +++ /dev/null @@ -1,15 +0,0 @@ -const test = require('node:test'); -const assert = require('node:assert/strict'); -const { canonicalJson, createRequestHash } = require('../../utils/canonicalJson'); - -test('sorts nested keys and preserves arrays', () => assert.equal( - canonicalJson({ z: [3, { b: true, a: null }], a: 'x' }), - '{"a":"x","z":[3,{"a":null,"b":true}]}' -)); - -test('hash ignores insertion order', () => assert.equal( - createRequestHash('demo.protected-action.v1', { b: 2, a: { y: 1, x: 0 } }), - createRequestHash('demo.protected-action.v1', { a: { x: 0, y: 1 }, b: 2 }) -)); - -test('rejects non-finite numbers', () => assert.throws(() => canonicalJson({ amount: NaN }), /finite/)); diff --git a/test/utils/playIntegrityDomain.test.js b/test/utils/playIntegrityDomain.test.js deleted file mode 100644 index a4e6ebb..0000000 --- a/test/utils/playIntegrityDomain.test.js +++ /dev/null @@ -1,19 +0,0 @@ -const test = require('node:test'); -const assert = require('node:assert/strict'); -const { isPlayIntegrityVerificationDomain } = require('../../utils/playIntegrityDomain'); - -test('matches only the configured full hostname', () => { - const domains = ['showcase.example.com', 'Demo.Example.com']; - - assert.equal(isPlayIntegrityVerificationDomain('showcase.example.com', domains), true); - assert.equal(isPlayIntegrityVerificationDomain('demo.example.com', domains), true); - assert.equal(isPlayIntegrityVerificationDomain('www.showcase.example.com', domains), false); - assert.equal(isPlayIntegrityVerificationDomain('example.com', domains), false); - assert.equal(isPlayIntegrityVerificationDomain('https://showcase.example.com', domains), false); -}); - -test('does not enable verification for an invalid or missing domain list', () => { - assert.equal(isPlayIntegrityVerificationDomain('showcase.example.com', undefined), false); - assert.equal(isPlayIntegrityVerificationDomain('showcase.example.com', []), false); - assert.equal(isPlayIntegrityVerificationDomain('', ['showcase.example.com']), false); -}); diff --git a/utils/authServerHelper.js b/utils/authServerHelper.js deleted file mode 100644 index 878e8c3..0000000 --- a/utils/authServerHelper.js +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Auth Server Helper - * Utility functions for working with auth servers configuration - */ - -/** - * Get auth server by server_id - * @param {Array} authServers - Array of auth server configurations - * @param {string} serverId - Server ID to lookup (optional) - * @returns {Object|null} Auth server object with {id, url} or null if not found - * - * @example - * const server = getAuthServer(authServers, 'stage'); - * // Returns: { id: 'stage', url: 'https://api.stage.ipification.com/auth' } - * - * @example - * const server = getAuthServer(authServers); // no serverId - * // Returns first server: { id: 'stage', url: '...' } - */ -function getAuthServer(authServers, serverId) { - // If no serverId provided, return first server as default - if (!serverId) { - return authServers && authServers.length > 0 ? authServers[0] : null; - } - - // Find server by id - const server = authServers.find(s => s.id === serverId); - return server || null; -} - -/** - * Get all available auth server IDs - * @param {Array} authServers - Array of auth server configurations - * @returns {Array} Array of server IDs - * - * @example - * const ids = getAuthServerIds(authServers); - * // Returns: ['stage', 'live'] - */ -function getAuthServerIds(authServers) { - if (!authServers || !Array.isArray(authServers)) { - return []; - } - return authServers.map(s => s.id); -} - -/** - * Check if a server ID exists - * @param {Array} authServers - Array of auth server configurations - * @param {string} serverId - Server ID to check - * @returns {boolean} True if server exists - * - * @example - * const exists = hasAuthServer(authServers, 'stage'); - * // Returns: true - */ -function hasAuthServer(authServers, serverId) { - if (!authServers || !Array.isArray(authServers) || !serverId) { - return false; - } - return authServers.some(s => s.id === serverId); -} - -module.exports = { - getAuthServer, - getAuthServerIds, - hasAuthServer -}; diff --git a/utils/canonicalJson.js b/utils/canonicalJson.js deleted file mode 100644 index fae2f72..0000000 --- a/utils/canonicalJson.js +++ /dev/null @@ -1,67 +0,0 @@ -const crypto = require('node:crypto'); - -function canonicalJson(value) { - return serialize(value, new Set()); -} - -function serialize(value, ancestors) { - if (value === null) return 'null'; - - switch (typeof value) { - case 'string': - return JSON.stringify(value); - case 'boolean': - return value ? 'true' : 'false'; - case 'number': - if (!Number.isFinite(value)) throw new TypeError('numbers must be finite'); - return JSON.stringify(value); - case 'undefined': - case 'function': - case 'symbol': - case 'bigint': - throw new TypeError(`unsupported value type: ${typeof value}`); - case 'object': - break; - default: - throw new TypeError(`unsupported value type: ${typeof value}`); - } - - if (ancestors.has(value)) throw new TypeError('cannot serialize circular structures'); - ancestors.add(value); - let result; - - if (Array.isArray(value)) { - const items = []; - for (let index = 0; index < value.length; index += 1) { - if (!Object.prototype.hasOwnProperty.call(value, index)) { - throw new TypeError('unsupported undefined array element'); - } - items.push(serialize(value[index], ancestors)); - } - result = `[${items.join(',')}]`; - } else { - const prototype = Object.getPrototypeOf(value); - if (prototype !== Object.prototype && prototype !== null) { - throw new TypeError('value must contain only plain objects'); - } - if (Object.getOwnPropertySymbols(value).length > 0) { - throw new TypeError('symbols are not supported'); - } - const keys = Object.keys(value).sort(); - result = `{${keys.map((key) => `${JSON.stringify(key)}:${serialize(value[key], ancestors)}`).join(',')}}`; - } - - ancestors.delete(value); - return result; -} - -function createRequestHash(action, payload) { - if (typeof action !== 'string' || action.length === 0) { - throw new TypeError('action must be a non-empty string'); - } - return crypto.createHash('sha256') - .update(`${action}\n${canonicalJson(payload)}`, 'utf8') - .digest('base64url'); -} - -module.exports = { canonicalJson, createRequestHash }; diff --git a/utils/constants.js b/utils/constants.js deleted file mode 100644 index be32349..0000000 --- a/utils/constants.js +++ /dev/null @@ -1,23 +0,0 @@ -const QR_CODE_SUFFIX = '-qrcode'; - -const HTTP_STATUS = { - OK: 200, - BAD_REQUEST: 400, - UNAUTHORIZED: 401, - INTERNAL_SERVER_ERROR: 500 -}; - -const ERROR_MESSAGES = { - CLIENT_NOT_FOUND: 'Client not found' -}; - -const MAX_RETRY_ATTEMPTS = 4; -const RETRY_DELAY_MS = 1000; - -module.exports = { - QR_CODE_SUFFIX, - HTTP_STATUS, - ERROR_MESSAGES, - MAX_RETRY_ATTEMPTS, - RETRY_DELAY_MS -}; diff --git a/utils/helpers.js b/utils/helpers.js deleted file mode 100644 index 8c4a459..0000000 --- a/utils/helpers.js +++ /dev/null @@ -1,42 +0,0 @@ -function deepMerge(target, source) { - const output = { ...target }; - if (isObject(target) && isObject(source)) { - Object.keys(source).forEach(key => { - if (isObject(source[key])) { - if (!(key in target)) { - Object.assign(output, { [key]: source[key] }); - } else { - output[key] = deepMerge(target[key], source[key]); - } - } else { - Object.assign(output, { [key]: source[key] }); - } - }); - } - return output; -} - -function isObject(item) { - return item && typeof item === 'object' && !Array.isArray(item); -} - -function delay(time) { - return new Promise(resolve => setTimeout(resolve, time)); -} - -function findClientByUserFlow(clients, userFlow) { - return clients.find(item => item.user_flow === userFlow); -} - -function findClientByClientId(clients, clientId) { - return clients.find(item => item.client_id === clientId); -} - -module.exports = { - deepMerge, - isObject, - delay, - findClientByUserFlow, - findClientByClientId -}; - diff --git a/utils/httpClient.js b/utils/httpClient.js deleted file mode 100644 index 6c2e8ed..0000000 --- a/utils/httpClient.js +++ /dev/null @@ -1,42 +0,0 @@ -const axios = require('axios'); -const qs = require('qs'); - -const getFormUrlEncodedConfig = () => ({ - headers: { 'Content-Type': 'application/x-www-form-urlencoded' } -}); - -const getJsonConfig = () => ({ - headers: { 'Content-Type': 'application/json' } -}); - -async function exchangeCodeForToken(tokenUrl, params) { - const config = getFormUrlEncodedConfig(); - const { data: tokenInfo } = await axios.post(tokenUrl, qs.stringify(params), config); - return tokenInfo; -} - -async function getUserInfo(userUrl, accessToken) { - const config = getFormUrlEncodedConfig(); - const { data: userInfo } = await axios.post( - userUrl, - qs.stringify({ access_token: accessToken }), - config - ); - return userInfo; -} - -async function exchangeCodeAndGetUserInfo(tokenUrl, userUrl, params) { - const tokenInfo = await exchangeCodeForToken(tokenUrl, params); - const { access_token: accessToken } = tokenInfo; - const userInfo = await getUserInfo(userUrl, accessToken); - return { tokenInfo, userInfo }; -} - -module.exports = { - getFormUrlEncodedConfig, - getJsonConfig, - exchangeCodeForToken, - getUserInfo, - exchangeCodeAndGetUserInfo -}; - diff --git a/utils/logger.js b/utils/logger.js deleted file mode 100644 index 26392c8..0000000 --- a/utils/logger.js +++ /dev/null @@ -1,26 +0,0 @@ -function timestamp() { - return new Date().toISOString(); -} - -function log(...args) { - console.log(`[${timestamp()}]`, ...args); -} - -function info(...args) { - console.info(`[${timestamp()}]`, ...args); -} - -function warn(...args) { - console.warn(`[${timestamp()}]`, ...args); -} - -function error(...args) { - console.error(`[${timestamp()}]`, ...args); -} - -module.exports = { - log, - info, - warn, - error -}; diff --git a/utils/phoneNumber.js b/utils/phoneNumber.js deleted file mode 100644 index d4d67f3..0000000 --- a/utils/phoneNumber.js +++ /dev/null @@ -1,13 +0,0 @@ -function normalizePhoneNumber(phoneNumber) { - if (typeof phoneNumber !== 'string') { - throw new TypeError('phone number must be an E.164 string'); - } - - const normalized = phoneNumber.trim().replace(/[\s().-]/g, ''); - if (!/^\+[1-9]\d{7,14}$/.test(normalized)) { - throw new TypeError('phone number must be a valid E.164 value'); - } - return normalized; -} - -module.exports = { normalizePhoneNumber }; diff --git a/utils/playIntegrityDomain.js b/utils/playIntegrityDomain.js deleted file mode 100644 index 1284057..0000000 --- a/utils/playIntegrityDomain.js +++ /dev/null @@ -1,9 +0,0 @@ -function isPlayIntegrityVerificationDomain(hostname, domains) { - if (typeof hostname !== 'string' || hostname.trim() === '' || !Array.isArray(domains)) return false; - - const normalizedHostname = hostname.trim().toLowerCase(); - return domains.some((domain) => typeof domain === 'string' - && domain.trim().toLowerCase() === normalizedHostname); -} - -module.exports = { isPlayIntegrityVerificationDomain }; diff --git a/utils/validateConfig.js b/utils/validateConfig.js deleted file mode 100644 index 719f68a..0000000 --- a/utils/validateConfig.js +++ /dev/null @@ -1,134 +0,0 @@ -/** - * Configuration validation module - * Validates application configuration on startup - */ -const logger = require('./logger'); - -/** - * Validate auth_servers configuration - * @param {Array} authServers - Array of auth server configurations - * @throws {Error} If validation fails - */ -function validateAuthServers(authServers) { - if (!authServers || !Array.isArray(authServers)) { - logger.log('auth_servers', authServers); - throw new Error('Configuration error: auth_servers must be defined as an array in config'); - } - - if (authServers.length === 0) { - logger.log('auth_servers', authServers); - throw new Error('Configuration error: auth_servers must contain at least one server'); - } - - // Validate each auth server has required fields - authServers.forEach((server, index) => { - if (!server.id || typeof server.id !== 'string') { - throw new Error(`Configuration error: auth_servers[${index}] must have an 'id' (string)`); - } - if (!server.url || typeof server.url !== 'string') { - throw new Error(`Configuration error: auth_servers[${index}] must have a 'url' (string)`); - } - // Validate URL format - try { - new URL(server.url); - } catch (err) { - throw new Error(`Configuration error: auth_servers[${index}].url is not a valid URL: ${server.url}`); - } - }); - - // Check for duplicate IDs - const ids = authServers.map(s => s.id); - const duplicates = ids.filter((id, index) => ids.indexOf(id) !== index); - if (duplicates.length > 0) { - throw new Error(`Configuration error: Duplicate auth server IDs found: ${duplicates.join(', ')}`); - } - - logger.log(`✓ Auth servers configured: ${authServers.map(s => s.id).join(', ')}`); -} - -/** - * Validate clients configuration - * @param {Array} clients - Array of client configurations - * @throws {Error} If validation fails - */ -function validateClients(clients) { - if (!clients || !Array.isArray(clients)) { - throw new Error('Configuration error: clients must be defined as an array in config'); - } - - if (clients.length === 0) { - throw new Error('Configuration error: clients must contain at least one client'); - } - - clients.forEach((client, index) => { - if (!client.user_flow || typeof client.user_flow !== 'string') { - throw new Error(`Configuration error: clients[${index}] must have a 'user_flow' (string)`); - } - if (!client.scope || typeof client.scope !== 'string') { - throw new Error(`Configuration error: clients[${index}] must have a 'scope' (string)`); - } - }); - - // Check for duplicate user_flow - const userFlows = clients.map(c => c.user_flow); - const duplicates = userFlows.filter((flow, index) => userFlows.indexOf(flow) !== index); - if (duplicates.length > 0) { - throw new Error(`Configuration error: Duplicate user_flow found: ${duplicates.join(', ')}`); - } - - logger.log(`✓ Clients configured: ${clients.length} client(s)`); -} - -/** - * Validate realm configuration - * @param {string} realm - Realm name - * @throws {Error} If validation fails - */ -function validateRealm(realm) { - if (!realm || typeof realm !== 'string') { - throw new Error('Configuration error: realm must be defined as a string in config'); - } - logger.log(`✓ Realm configured: ${realm}`); -} - -/** - * Validate credentials configuration - * @param {Object} config - Configuration object - * @throws {Error} If validation fails - */ -function validateCredentials(config) { - if (!config.client_id || typeof config.client_id !== 'string') { - throw new Error('Configuration error: client_id must be defined as a string'); - } - - if (!config.client_secret || typeof config.client_secret !== 'string') { - throw new Error('Configuration error: client_secret must be defined as a string'); - } - - logger.log('✓ Credentials configured'); -} - -/** - * Main validation function - * Validates all required configuration - * @param {Object} config - Application configuration object - * @throws {Error} If any validation fails - */ -function validateConfig(config) { - logger.log('\n=== Validating Configuration ==='); - - validateRealm(config.realm); - validateAuthServers(config.auth_servers); - validateClients(config.clients); - validateCredentials(config); - - logger.log('=== Configuration Valid ✓ ===\n'); -} - -module.exports = { - validateConfig, - validateAuthServers, - validateClients, - validateRealm, - validateCredentials -}; diff --git a/views/404.pug b/views/404.pug deleted file mode 100644 index 9645659..0000000 --- a/views/404.pug +++ /dev/null @@ -1,5 +0,0 @@ - -extends layout - -block content - #four04 I'm sorry, the page or resource you are searching for is currently unavailable. \ No newline at end of file diff --git a/views/info.pug b/views/info.pug deleted file mode 100644 index 29bda37..0000000 --- a/views/info.pug +++ /dev/null @@ -1,21 +0,0 @@ - -extends layout - -block content - .wrapper_response - .container.d-flex.h-100.w-100.mx-auto.flex-column.p-0 - main - .response-block - h5.wrapper_title !{client_title} - a(href='/') - button.close(type='button' data-dismiss='modal' aria-label='Close') - span(aria-hidden='true').right-icon - div.mb-5 - code!= userInfo - footer(class="mt-auto") - div.footer_response - div - p.margin_helper client id: #{client_id} - div - p.margin_helper state: #{state} - diff --git a/views/layout.pug b/views/layout.pug deleted file mode 100644 index 18a56ec..0000000 --- a/views/layout.pug +++ /dev/null @@ -1,31 +0,0 @@ -doctype html -html(lang="en") - head - title= title - meta(charset='utf-8') - link(rel="icon" href=favicon_url || "/images/logo.ico" type="image/x-icon") - meta(name='viewport' content='width=device-width,initial-scale=1,user-scalable=no, shrink-to-fit=no') - meta(name='apple-mobile-web-app-capable' content='yes') - meta(name='apple-mobile-web-app-status-bar-style' content='black') - meta(http-equiv='Content-Type' content='text/html; charset=UTF-8') - link(rel="stylesheet", href=`/css/lib/bootstrap.min.css`) - link(rel='stylesheet', href=`/css/animate.css`) - link(rel='stylesheet', href=`/css/style.css?v=3`) - link(rel="stylesheet" href="/js/lib/intl-tel-input-24.6.0/css/intlTelInput.min.css") - body - block content - script. - var default_country_code = '#{default_country_code}' ; - var app_env = '#{app_env}'; - var disabled_select_country = !{JSON.stringify(!!disabled_select_country)}; - script(src='/js/lib/code.min.js') - script(src='/js/lib/sweetalert.all.min.js') - script(src="/js/lib/socket.io.js") - script(src="/js/lib/jquery-3.7.1.min.js") - script(src="/js/lib/bootstrap.min.js") - script(src="/js/lib/intl-tel-input-24.6.0/js/intlTelInputWithUtils.min.js") - script(src="/js/lib/bowser-2.11.0.min.js") - block head_scripts - script(src=`/js/login.js?v=3`) - script(src=`/js/socket.js`) - block scripts diff --git a/views/login.pug b/views/login.pug deleted file mode 100644 index 35d0bab..0000000 --- a/views/login.pug +++ /dev/null @@ -1,173 +0,0 @@ - -extends layout - -block content - - const pvnFlows = ['pvn_ip', 'pvn_ip_plus', 'pvn_im', 'pvn_ipificator', 'pvn_sim', 'pvn_sms']; - - const loginFlows = ['login_ip', 'login_ip_plus', 'login_im', 'login_sim']; - - const identityFlows = ['anonymous', 'kyc_phone']; - - const hasPvnButtons = pvnFlows.some(flow => clients.find(c => c.user_flow === flow)); - - const hasLoginButtons = loginFlows.some(flow => clients.find(c => c.user_flow === flow)); - - const hasIdentityButtons = identityFlows.some(flow => clients.find(c => c.user_flow === flow)); - - const firstVisibleTab = hasPvnButtons ? 'pnv' : (hasLoginButtons ? 'login' : (hasIdentityButtons ? 'identity' : 'pnv')); - - .container.d-flex.h-100.w-100.mx-auto.flex-column - main - #login.center-vertical - div.form-login - .clearfix - .row.block-header - .col.col-sm-6.col-md-8 - .row - if app_logo_url - img.app-logo(src=app_logo_url alt=locale.app.name style=app_logo_style || 'max-width: 100%') - else - h3.app-name #{locale.app.name} - .col.col-sm-6.col-md-4 - if auth_servers.length > 1 - .row.auth-server-selector - .label Site - select.form-control(id="select_auth_server") - each auth_server in auth_servers - option(value=auth_server.id) #{auth_server.id} - .row.block-page - ul.list-tabs.nav.nav-tabs - if hasPvnButtons - li.nav-item - a.nav-link.nav-link-pnv(href="#pnv" onClick="select_nav('pnv')" class=firstVisibleTab === 'pnv' ? 'active' : '') #{locale.tabs.pnv} - if hasLoginButtons - li.nav-item - a.nav-link.nav-link-login(href="#login" onClick="select_nav('login')" class=firstVisibleTab === 'login' ? 'active' : '') #{locale.tabs.login} - if hasIdentityButtons - li.nav-item - a.nav-link.nav-link-identity(href="#identity" onClick="select_nav('identity')" class=firstVisibleTab === 'identity' ? 'active' : '') #{locale.tabs.identity} - - .col-12.row-item - if hasPvnButtons - .block-button.block.block-pnv.block-1#pnv(class=firstVisibleTab === 'pnv' ? 'active' : '') - p.description_title #{locale.sections.pnv.title} - p.description !{locale.sections.pnv.description} - .div(style="margin-top: 15px") - .row - .col - input.form-control.phoneNumber(type='text' name='phone' id='phone') - each flow in pvnFlows - - const client = clients.find(c => c.user_flow === flow); - if client - if flow === 'pvn_ip' - button.btn_full.btn.btn_phone.btn-active.btnLoginPhone.btn-user-flow#pvn_ip(data-title=get_flow_title('pvn_ip', locale.sections.pnv.title) data-user-flow='pvn_ip' data-client-id=client.client_id) #{locale.sections.pnv.button_ip_label || locale.buttons.ip} - span.right-icon - else if flow === 'pvn_ip_plus' - button.btn_full.btn.btn_phone.btn-active.btnLoginPhone.btn-user-flow#pvn_ip_plus(data-title=get_flow_title('pvn_ip_plus', locale.sections.pnv.title) data-user-flow='pvn_ip_plus' data-client-id=client.client_id) #{locale.sections.pnv.button_ip_plus_label || locale.buttons.ip_plus} - span.right-icon - else if flow === 'pvn_im' - button.btn_full.btn.btn_IM.btnLoginPhone.btn-user-flow#pvn_im(data-title=get_flow_title('pvn_im', locale.sections.pnv.title) data-user-flow='pvn_im' data-client-id=client.client_id) #{locale.sections.pnv.button_im_label || locale.buttons.im} - span.right-icon - else if flow === 'pvn_ipificator' - button.btn_full.btn.btn_IM.btnLoginPhone.btn-user-flow#pvn_ipificator(data-title=get_flow_title('pvn_ipificator', locale.sections.pnv.title) data-user-flow='pvn_ipificator' data-client-id=client.client_id, style="display: none") #{locale.sections.pnv.button_ip_via_app_label || locale.buttons.ip_via_app} - span.right-icon - else if flow === 'pvn_sim' - button.btn_full.btn.btn_IM.btnLoginPhone.btn-user-flow#pvn_sim(data-title=get_flow_title('pvn_sim', locale.sections.pnv.title) data-user-flow='pvn_sim' data-client-id=client.client_id) #{locale.sections.pnv.button_sim_label || locale.buttons.sim} - span.right-icon - else if flow === 'pvn_sms' - button.btn_full.btn.btn_sms.btnLoginPhone.btn-user-flow#pvn_sms(data-title=get_flow_title('pvn_sms', locale.sections.pnv.title) data-user-flow='pvn_sms' data-client-id=client.client_id) #{locale.sections.pnv.button_sms_label || locale.buttons.sms} - span.right-icon - - if hasLoginButtons - .block-button.block-login.block.block-2#login(class=firstVisibleTab === 'login' ? 'active' : '') - p.description_title #{locale.sections.login.title} - p.description.description_helper !{locale.sections.login.description} - each flow in loginFlows - - const client = clients.find(c => c.user_flow === flow); - if client - if flow === 'login_ip' - button.btn_full_blue.btn_instant.btn-user-flow#login_ip(type='button' data-title=get_flow_title('login_ip', locale.sections.login.title) data-user-flow='login_ip' data-client-id=client.client_id) - span.right-icon - span #{locale.sections.login.button_ip_label || locale.buttons.ip} - else if flow === 'login_ip_plus' - button.btn_full_blue.btn_instant.btn-user-flow#login_ip_plus(type='button' data-title=get_flow_title('login_ip_plus', locale.sections.login.title) data-user-flow='login_ip_plus' data-client-id=client.client_id) - span.right-icon - span #{locale.sections.login.button_ip_plus_label || locale.buttons.ip_plus} - else if flow === 'login_im' - button.btn_full_blue.btn-user-flow#login_im(type='button' data-title=get_flow_title('login_im', locale.sections.login.title) data-user-flow='login_im' data-client-id=client.client_id) - span.right-icon - span #{locale.sections.login.button_im_label || locale.buttons.im} - else if flow === 'login_sim' - button.btn_full_blue.btn-user-flow#login_sim(type='button' data-title=get_flow_title('login_sim', locale.buttons.sim) data-user-flow='login_sim' data-client-id=client.client_id) - span.right-icon - span #{locale.sections.login.button_sim_label || locale.buttons.sim} - - if hasIdentityButtons - .block-button.block.block-identity.block-1#identity(class=firstVisibleTab === 'identity' ? 'active' : '') - p.description_title #{locale.sections.identity.title} - p.description.description_helper !{locale.sections.identity.description} - - const anonymousClient = clients.find(c => c.user_flow === 'anonymous'); - if anonymousClient - button.btn_full_blue.btn_instant.btn-user-flow#anonymous(type='button' data-title=get_flow_title('anonymous', locale.sections.identity.title) style="margin-bottom:59px" data-user-flow='anonymous' data-client-id=anonymousClient.client_id) - span.right-icon.right-icon-blue - span #{locale.sections.identity.button_anonymous_identity_label || locale.buttons.anonymous_identity} - - const kycClient = clients.find(c => c.user_flow === 'kyc_phone'); - if kycClient - .div - input.form-control.phoneNumber(type='text' name='phone_kyc' id='phone_kyc') - button.btn_full.btn.btn_kyc.btnLoginPhone.btn-user-flow#kyc_phone(data-title=get_flow_title('kyc_phone', locale.sections.identity.title) type='button' data-user-flow='kyc_phone' data-client-id=kycClient.client_id) #{locale.sections.identity.button_kyc_label || locale.buttons.kyc} - span.right-icon - - footer(class="mt-auto") - .viettel-legal - | #{locale.footer.viettel_legal} - a(href="#" style="margin-left: 4px; margin-right: 4px;" onclick="showConsentPage()") #{locale.footer.viettel} - | in case you are Viettel subscribers. - .privacy-link - a(href="https://www.ipification.com/legal" target='_blank') - | #{locale.footer.privacy_policy} - span.info-icon - | #{locale.footer.about} - a(href="javascript:void(0)" id="help") - | #{locale.footer.help} - - #app_info.modal(tabindex="-1" role="dialog") - .modal-dialog - .modal-content(style="border-radius: 0px;") - .modal-header - h5.modal-title #{locale.modals.about_title} - button.close(type='button' data-dismiss='modal' aria-label='Close') - span(aria-hidden='true').right-icon - .modal-body - div.mb-3 - strong #{locale.modals.server_label} - p#selected_server_info - - .developer-link - a(href="https://developer.ipification.com/#/") - | #{locale.modals.developer_link} - - #input_alert.error-popup.modal(tabindex="-1" role="dialog") - .modal-dialog - .modal-content(style="border-radius: 0px;") - .modal-header - h6.modal-title #{locale.modals.phone_required} - - #phone_invalid_alert.error-popup.modal(tabindex="-1" role="dialog") - .modal-dialog - .modal-content(style="border-radius: 0px;") - .modal-header - h6.modal-title #{locale.modals.phone_invalid} - - .wrapper-loader - span.loader - span.loader-inner - - if error_message - #error_alert.modal(tabindex="-1" role="dialog") - .modal-dialog - .modal-content(style="border-radius: 0px;") - .modal-header - h6.modal-title - | #{error_message} - if error_state - br - strong - | State: #{error_state} - -block head_scripts - script(type="text/javascript"). - var base_url = "!{baseUrl}"; diff --git a/views/qr_error.pug b/views/qr_error.pug deleted file mode 100644 index 6c1a14b..0000000 --- a/views/qr_error.pug +++ /dev/null @@ -1,11 +0,0 @@ -extends layout - -block content - .response-block.text-center - .modal(style="display: block") - .modal-dialog-error(role='document') - .modal-content-error - .modal-body-error - strong.text-error Failure - p.description Authentication unsuccessful - span.error \ No newline at end of file diff --git a/views/qr_success.pug b/views/qr_success.pug deleted file mode 100644 index 124dd91..0000000 --- a/views/qr_success.pug +++ /dev/null @@ -1,11 +0,0 @@ -extends layout - -block content - .response-block.text-center - .modal(style="display: block") - .modal-dialog-success(role='document') - .modal-content-success - .modal-body-success - strong.text-success Success - p.description You've been authenticated - span.success \ No newline at end of file From 7cff56275dc0d6148e6a95f29c88438fb1c41f77 Mon Sep 17 00:00:00 2001 From: Manh Le Date: Sun, 1 Nov 2026 22:25:41 +0700 Subject: [PATCH 13/13] refactor: move Next.js showcase to repository root --- .dockerignore | 25 +- showcase-next/.env.example => .env.example | 5 +- .github/workflows/showcase-next-ci.yml | 62 ++ .gitignore | 19 +- Dockerfile | 31 +- README.md | 89 +- .../components.json => components.json | 0 .../config => config}/app_config.json | 0 .../config => config}/default.json.sample | 0 {showcase-next/config => config}/locale.json | 0 docker-compose.next.yml | 51 - docker-compose.yml | 50 +- docs/INDEX.md | 331 +------ docs/NEXTJS_AUTH_FLOWS.md | 171 ++++ docs/NEXTJS_BACKEND.md | 103 +- docs/NEXTJS_MIGRATION.md | 59 +- docs/NEXTJS_UI_PARITY.md | 59 ++ docs/PLAY_INTEGRITY.md | 2 + docs/PLAY_INTEGRITY_IPIFICATION_DEMO.md | 2 + docs/nextjs/README.md | 31 + docs/nextjs/api.md | 42 + docs/nextjs/browser-flows.md | 81 ++ docs/nextjs/qr.md | 36 + docs/nextjs/running.md | 45 + docs/nextjs/sim.md | 35 + docs/nextjs/sms.md | 35 + {showcase-next/e2e => e2e}/auth-error.spec.ts | 24 +- e2e/auth-status.spec.ts | 42 + {showcase-next/e2e => e2e}/bootstrap.spec.ts | 0 e2e/flow-routing.spec.ts | 86 ++ e2e/legacy-layout.spec.ts | 103 ++ e2e/login.spec.ts | 107 +++ e2e/main-flows.spec.ts | 76 ++ e2e/phone.spec.ts | 100 ++ e2e/qr-layout.spec.ts | 54 ++ e2e/qr.spec.ts | 209 ++++ e2e/result.spec.ts | 56 ++ {showcase-next/e2e => e2e}/sms.spec.ts | 0 {showcase-next/e2e => e2e}/stage.spec.ts | 7 +- e2e/tab-focus.spec.ts | 31 + {showcase-next/e2e => e2e}/ts43.spec.ts | 10 + e2e/usability.spec.ts | 173 ++++ {showcase-next/e2e => e2e}/visual.spec.ts | 0 .../eslint.config.mjs => eslint.config.mjs | 2 + showcase-next/next-env.d.ts => next-env.d.ts | 0 next.config.ts | 20 + .../package-lock.json => package-lock.json | 359 +------ showcase-next/package.json => package.json | 4 - ...aywright.config.ts => playwright.config.ts | 15 +- .../postcss.config.mjs => postcss.config.mjs | 0 public/fonts/INTER-LICENSE.txt | 92 ++ public/fonts/Inter-Bold.woff2 | Bin 0 -> 114840 bytes public/fonts/Inter-Medium.woff2 | Bin 0 -> 114348 bytes public/fonts/Inter-Regular.woff2 | Bin 0 -> 111268 bytes .../images/btn-active-next-blue.svg | 0 .../images/btn-active-next-gray.svg | 0 .../images/btn-active-next.svg | 0 .../public => public}/images/check.svg | 0 .../public => public}/images/close_btn.svg | 0 .../public => public}/images/error.svg | 0 .../public => public}/images/info.svg | 0 .../public => public}/images/logo.ico | Bin .../logo/ipification-horizontal-black.svg | 1 + .../public => public}/images/logo/vil.svg | 0 .../runtime => runtime}/config.d.mts | 0 {showcase-next/runtime => runtime}/config.mjs | 18 - .../scripts => scripts}/check-config.mjs | 0 showcase-next/server.mjs => server.mjs | 39 +- showcase-next/.dockerignore | 16 - showcase-next/.gitignore | 12 - showcase-next/Dockerfile | 29 - showcase-next/e2e/login.spec.ts | 35 - showcase-next/e2e/phone.spec.ts | 18 - showcase-next/e2e/qr.spec.ts | 45 - showcase-next/next.config.ts | 9 - showcase-next/runtime/socket-server.d.mts | 5 - showcase-next/runtime/socket-server.mjs | 39 - showcase-next/src/app/api/config/route.ts | 12 - showcase-next/src/app/api/geoip/route.ts | 16 - showcase-next/src/app/auth/complete/route.ts | 2 - .../src/app/auth/mobile/login/route.ts | 1 - .../src/app/auth/qrcode/complete/page.tsx | 11 - .../src/app/auth/qrcode/error/page.tsx | 11 - showcase-next/src/app/auth/start/route.ts | 2 - showcase-next/src/app/sms/auth/route.ts | 1 - showcase-next/src/app/sms/token/route.ts | 1 - showcase-next/src/app/ts43/auth/route.ts | 1 - showcase-next/src/app/ts43/token/route.ts | 1 - showcase-next/src/features/login/showcase.tsx | 347 ------- showcase-next/src/features/qr/qr-dialog.tsx | 62 -- .../src/features/qr/use-auth-socket.tsx | 51 - .../src/features/results/result-view.tsx | 26 - showcase-next/src/server/auth/service.ts | 139 --- showcase-next/src/server/auth/web.ts | 97 -- showcase-next/src/server/ciba/handlers.ts | 102 -- showcase-next/src/server/ciba/provider.ts | 140 --- showcase-next/src/server/ciba/service.ts | 118 --- showcase-next/src/server/ciba/sms.ts | 5 - showcase-next/src/server/ciba/ts43.ts | 5 - showcase-next/src/server/config/public.ts | 45 - showcase-next/src/server/integrity/service.ts | 146 --- showcase-next/src/server/realtime/notifier.ts | 20 - .../src/server/shared/http-client.ts | 28 - .../tests/contracts/bootstrap.test.ts | 16 - showcase-next/tests/contracts/parity.test.ts | 133 --- .../tests/contracts/web-auth.test.ts | 80 -- .../tests/integration/realtime.test.ts | 59 -- showcase-next/tests/services/auth.test.ts | 157 ---- showcase-next/tests/unit/notifier.test.ts | 17 - src/app/api/config/route.ts | 14 + src/app/api/geoip/route.ts | 20 + .../app/api/play-integrity/attempt/route.ts | 0 .../play-integrity/token-exchange/route.ts | 0 .../app/api/play-integrity/verify/route.ts | 0 .../callback/[userFlow]/[serverId]/route.ts | 4 +- src/app/auth/complete/route.ts | 2 + .../src => src}/app/auth/error/page.tsx | 3 +- .../src => src}/app/auth/login/error/route.ts | 1 + .../src => src}/app/auth/login/page.tsx | 1 - src/app/auth/mobile/login/route.ts | 1 + src/app/auth/pending/page.tsx | 12 + src/app/auth/prepare/route.ts | 1 + src/app/auth/qrcode/complete/page.tsx | 12 + src/app/auth/qrcode/error/page.tsx | 20 + .../src => src}/app/auth/s2s/signin/route.ts | 7 +- .../app/auth/session-required/route.ts | 0 src/app/auth/start/route.ts | 2 + src/app/auth/status/route.ts | 2 + {showcase-next/src => src}/app/globals.css | 85 +- .../src => src}/app/health/live/route.ts | 0 .../src => src}/app/health/ready/route.ts | 4 +- {showcase-next/src => src}/app/layout.tsx | 0 {showcase-next/src => src}/app/not-found.tsx | 0 {showcase-next/src => src}/app/route.ts | 0 src/app/sms/auth/route.ts | 1 + .../src => src}/app/sms/log/route.ts | 0 src/app/sms/token/route.ts | 1 + src/app/ts43/auth/route.ts | 1 + .../src => src}/app/ts43/log/route.ts | 0 src/app/ts43/token/route.ts | 1 + .../src => src}/app/user/info/page.tsx | 0 .../src => src}/components/ui/alert.tsx | 0 .../src => src}/components/ui/button.tsx | 9 +- .../src => src}/components/ui/card.tsx | 0 .../src => src}/components/ui/dialog.tsx | 0 .../src => src}/components/ui/input.tsx | 0 .../src => src}/components/ui/label.tsx | 0 .../src => src}/components/ui/select.tsx | 0 .../src => src}/components/ui/tabs.tsx | 0 src/features/auth/auth-status.ts | 151 +++ .../features/login/info-dialogs.tsx | 2 +- src/features/login/legacy-showcase.css | 889 ++++++++++++++++++ .../features/login/login-error.tsx | 23 +- src/features/login/pending-authentication.tsx | 67 ++ .../features/login/server-select.tsx | 0 src/features/login/showcase-layout.tsx | 126 +++ src/features/login/showcase.tsx | 236 +++++ .../features/login/use-preferences.tsx | 4 +- src/features/login/use-showcase-browser.ts | 63 ++ src/features/login/use-verification.ts | 211 +++++ .../features/phone/phone-input.tsx | 63 +- src/features/qr/qr-dialog.tsx | 140 +++ src/features/qr/use-auth-polling.tsx | 57 ++ src/features/results/copy-json.tsx | 26 + src/features/results/result-view.tsx | 80 ++ .../src => src}/features/sms/sms-dialog.tsx | 2 +- .../src => src}/features/sms/use-sms-flow.tsx | 3 + .../src => src}/features/sms/use-web-otp.tsx | 0 .../features/ts43/use-ts43-flow.tsx | 6 +- {showcase-next/src => src}/lib/utils.ts | 0 {showcase-next/src => src}/proxy.ts | 2 + src/server/auth/attempts.ts | 194 ++++ .../src => src}/server/auth/failure.ts | 24 +- .../src => src}/server/auth/input.ts | 6 + .../src => src}/server/auth/mobile.ts | 8 +- .../src => src}/server/auth/provider.ts | 9 +- .../src => src}/server/auth/repositories.ts | 0 src/server/auth/service.ts | 368 ++++++++ src/server/auth/state-cookies.ts | 39 + src/server/auth/web.ts | 216 +++++ src/server/ciba/handlers.ts | 167 ++++ .../src => src}/server/ciba/input.ts | 0 src/server/ciba/provider.ts | 162 ++++ src/server/ciba/service.ts | 202 ++++ .../src => src}/server/config/load.ts | 0 src/server/config/public.ts | 57 ++ .../src => src}/server/http/body.ts | 0 .../src => src}/server/http/context.ts | 7 +- .../src => src}/server/http/headers.ts | 0 .../src => src}/server/http/response.ts | 0 .../src => src}/server/integrity/attempts.ts | 0 .../src => src}/server/integrity/handlers.ts | 0 .../src => src}/server/integrity/input.ts | 19 +- .../src => src}/server/integrity/ports.ts | 0 .../src => src}/server/integrity/runtime.ts | 0 src/server/integrity/service.ts | 175 ++++ .../src => src}/server/integrity/verifier.ts | 0 .../src => src}/server/session/cookies.ts | 0 .../src => src}/server/session/store.ts | 0 .../server/shared/canonical-json.ts | 0 .../src => src}/server/shared/context.ts | 0 .../src => src}/server/shared/errors.ts | 32 + src/server/shared/http-client.ts | 89 ++ .../src => src}/server/shared/logger.ts | 0 .../src => src}/server/shared/phone-number.ts | 0 .../src => src}/server/store/auth-results.ts | 2 + .../src => src}/server/store/integrity.ts | 0 .../src => src}/server/store/rate-limit.ts | 0 .../src => src}/server/store/redis.ts | 0 .../src => src}/shared/auth-failures.ts | 38 + .../src => src}/shared/contracts.ts | 1 + .../src => src}/shared/flow-catalog.ts | 0 .../contracts/auth-error.test.ts | 19 +- tests/contracts/bootstrap.test.ts | 85 ++ .../tests => tests}/contracts/ciba.test.ts | 36 +- .../contracts/config-geoip.test.ts | 6 +- .../contracts/integrity.test.ts | 1 - .../contracts/mobile-s2s.test.ts | 32 +- tests/contracts/oauth-state.test.ts | 208 ++++ tests/contracts/parity.test.ts | 52 + tests/contracts/production-origin.test.ts | 78 ++ tests/contracts/web-auth.test.ts | 229 +++++ tests/helpers/auth-attempt.ts | 28 + .../tests => tests}/helpers/config.ts | 1 + .../tests => tests}/helpers/e2e-server.mjs | 23 +- .../tests => tests}/helpers/processes.ts | 0 .../tests => tests}/helpers/provider.ts | 12 +- .../tests => tests}/helpers/redis.ts | 0 .../tests => tests}/helpers/server-only.ts | 0 .../helpers/service-context.ts | 0 .../integration/auth-results.test.ts | 0 .../integration/config-runtime.test.ts | 1 - .../integration/cross-instance.test.ts | 80 +- .../integration/integrity-concurrency.test.ts | 0 .../integration/production-runtime.test.ts | 2 - .../integration/rate-limit.test.ts | 0 .../integration/sessions.test.ts | 0 tests/services/auth.test.ts | 351 +++++++ .../tests => tests}/services/ciba.test.ts | 36 +- .../services/integrity.test.ts | 95 ++ .../tests => tests}/unit/auth-failure.test.ts | 0 .../tests => tests}/unit/auth-results.test.ts | 0 tests/unit/auth-status.test.ts | 170 ++++ .../unit/canonical-json.test.ts | 0 .../tests => tests}/unit/config.test.ts | 1 - .../tests => tests}/unit/context.test.ts | 6 +- .../unit/integrity-verifier.test.ts | 0 .../unit/provider-error.test.ts | 0 tests/unit/provider-timeout.test.ts | 80 ++ tests/unit/public-callbacks.test.ts | 21 + tests/unit/token-response.test.ts | 54 ++ showcase-next/tsconfig.json => tsconfig.json | 27 +- .../vitest.config.ts => vitest.config.ts | 0 253 files changed, 7844 insertions(+), 3112 deletions(-) mode change 100755 => 100644 .dockerignore rename showcase-next/.env.example => .env.example (53%) create mode 100644 .github/workflows/showcase-next-ci.yml rename showcase-next/components.json => components.json (100%) rename {showcase-next/config => config}/app_config.json (100%) rename {showcase-next/config => config}/default.json.sample (100%) rename {showcase-next/config => config}/locale.json (100%) delete mode 100644 docker-compose.next.yml create mode 100644 docs/NEXTJS_AUTH_FLOWS.md create mode 100644 docs/NEXTJS_UI_PARITY.md create mode 100644 docs/nextjs/README.md create mode 100644 docs/nextjs/api.md create mode 100644 docs/nextjs/browser-flows.md create mode 100644 docs/nextjs/qr.md create mode 100644 docs/nextjs/running.md create mode 100644 docs/nextjs/sim.md create mode 100644 docs/nextjs/sms.md rename {showcase-next/e2e => e2e}/auth-error.spec.ts (56%) create mode 100644 e2e/auth-status.spec.ts rename {showcase-next/e2e => e2e}/bootstrap.spec.ts (100%) create mode 100644 e2e/flow-routing.spec.ts create mode 100644 e2e/legacy-layout.spec.ts create mode 100644 e2e/login.spec.ts create mode 100644 e2e/main-flows.spec.ts create mode 100644 e2e/phone.spec.ts create mode 100644 e2e/qr-layout.spec.ts create mode 100644 e2e/qr.spec.ts create mode 100644 e2e/result.spec.ts rename {showcase-next/e2e => e2e}/sms.spec.ts (100%) rename {showcase-next/e2e => e2e}/stage.spec.ts (82%) create mode 100644 e2e/tab-focus.spec.ts rename {showcase-next/e2e => e2e}/ts43.spec.ts (68%) create mode 100644 e2e/usability.spec.ts rename {showcase-next/e2e => e2e}/visual.spec.ts (100%) rename showcase-next/eslint.config.mjs => eslint.config.mjs (89%) rename showcase-next/next-env.d.ts => next-env.d.ts (100%) create mode 100644 next.config.ts rename showcase-next/package-lock.json => package-lock.json (96%) rename showcase-next/package.json => package.json (92%) rename showcase-next/playwright.config.ts => playwright.config.ts (56%) rename showcase-next/postcss.config.mjs => postcss.config.mjs (100%) create mode 100644 public/fonts/INTER-LICENSE.txt create mode 100644 public/fonts/Inter-Bold.woff2 create mode 100644 public/fonts/Inter-Medium.woff2 create mode 100644 public/fonts/Inter-Regular.woff2 rename {showcase-next/public => public}/images/btn-active-next-blue.svg (100%) rename {showcase-next/public => public}/images/btn-active-next-gray.svg (100%) rename {showcase-next/public => public}/images/btn-active-next.svg (100%) rename {showcase-next/public => public}/images/check.svg (100%) rename {showcase-next/public => public}/images/close_btn.svg (100%) rename {showcase-next/public => public}/images/error.svg (100%) rename {showcase-next/public => public}/images/info.svg (100%) rename {showcase-next/public => public}/images/logo.ico (100%) create mode 100644 public/images/logo/ipification-horizontal-black.svg rename {showcase-next/public => public}/images/logo/vil.svg (100%) rename {showcase-next/runtime => runtime}/config.d.mts (100%) rename {showcase-next/runtime => runtime}/config.mjs (88%) rename {showcase-next/scripts => scripts}/check-config.mjs (100%) rename showcase-next/server.mjs => server.mjs (67%) delete mode 100644 showcase-next/.dockerignore delete mode 100644 showcase-next/.gitignore delete mode 100644 showcase-next/Dockerfile delete mode 100644 showcase-next/e2e/login.spec.ts delete mode 100644 showcase-next/e2e/phone.spec.ts delete mode 100644 showcase-next/e2e/qr.spec.ts delete mode 100644 showcase-next/next.config.ts delete mode 100644 showcase-next/runtime/socket-server.d.mts delete mode 100644 showcase-next/runtime/socket-server.mjs delete mode 100644 showcase-next/src/app/api/config/route.ts delete mode 100644 showcase-next/src/app/api/geoip/route.ts delete mode 100644 showcase-next/src/app/auth/complete/route.ts delete mode 100644 showcase-next/src/app/auth/mobile/login/route.ts delete mode 100644 showcase-next/src/app/auth/qrcode/complete/page.tsx delete mode 100644 showcase-next/src/app/auth/qrcode/error/page.tsx delete mode 100644 showcase-next/src/app/auth/start/route.ts delete mode 100644 showcase-next/src/app/sms/auth/route.ts delete mode 100644 showcase-next/src/app/sms/token/route.ts delete mode 100644 showcase-next/src/app/ts43/auth/route.ts delete mode 100644 showcase-next/src/app/ts43/token/route.ts delete mode 100644 showcase-next/src/features/login/showcase.tsx delete mode 100644 showcase-next/src/features/qr/qr-dialog.tsx delete mode 100644 showcase-next/src/features/qr/use-auth-socket.tsx delete mode 100644 showcase-next/src/features/results/result-view.tsx delete mode 100644 showcase-next/src/server/auth/service.ts delete mode 100644 showcase-next/src/server/auth/web.ts delete mode 100644 showcase-next/src/server/ciba/handlers.ts delete mode 100644 showcase-next/src/server/ciba/provider.ts delete mode 100644 showcase-next/src/server/ciba/service.ts delete mode 100644 showcase-next/src/server/ciba/sms.ts delete mode 100644 showcase-next/src/server/ciba/ts43.ts delete mode 100644 showcase-next/src/server/config/public.ts delete mode 100644 showcase-next/src/server/integrity/service.ts delete mode 100644 showcase-next/src/server/realtime/notifier.ts delete mode 100644 showcase-next/src/server/shared/http-client.ts delete mode 100644 showcase-next/tests/contracts/bootstrap.test.ts delete mode 100644 showcase-next/tests/contracts/parity.test.ts delete mode 100644 showcase-next/tests/contracts/web-auth.test.ts delete mode 100644 showcase-next/tests/integration/realtime.test.ts delete mode 100644 showcase-next/tests/services/auth.test.ts delete mode 100644 showcase-next/tests/unit/notifier.test.ts create mode 100644 src/app/api/config/route.ts create mode 100644 src/app/api/geoip/route.ts rename {showcase-next/src => src}/app/api/play-integrity/attempt/route.ts (100%) rename {showcase-next/src => src}/app/api/play-integrity/token-exchange/route.ts (100%) rename {showcase-next/src => src}/app/api/play-integrity/verify/route.ts (100%) rename {showcase-next/src => src}/app/auth/callback/[userFlow]/[serverId]/route.ts (55%) create mode 100644 src/app/auth/complete/route.ts rename {showcase-next/src => src}/app/auth/error/page.tsx (96%) rename {showcase-next/src => src}/app/auth/login/error/route.ts (83%) rename {showcase-next/src => src}/app/auth/login/page.tsx (97%) create mode 100644 src/app/auth/mobile/login/route.ts create mode 100644 src/app/auth/pending/page.tsx create mode 100644 src/app/auth/prepare/route.ts create mode 100644 src/app/auth/qrcode/complete/page.tsx create mode 100644 src/app/auth/qrcode/error/page.tsx rename {showcase-next/src => src}/app/auth/s2s/signin/route.ts (59%) rename {showcase-next/src => src}/app/auth/session-required/route.ts (100%) create mode 100644 src/app/auth/start/route.ts create mode 100644 src/app/auth/status/route.ts rename {showcase-next/src => src}/app/globals.css (56%) rename {showcase-next/src => src}/app/health/live/route.ts (100%) rename {showcase-next/src => src}/app/health/ready/route.ts (74%) rename {showcase-next/src => src}/app/layout.tsx (100%) rename {showcase-next/src => src}/app/not-found.tsx (100%) rename {showcase-next/src => src}/app/route.ts (100%) create mode 100644 src/app/sms/auth/route.ts rename {showcase-next/src => src}/app/sms/log/route.ts (100%) create mode 100644 src/app/sms/token/route.ts create mode 100644 src/app/ts43/auth/route.ts rename {showcase-next/src => src}/app/ts43/log/route.ts (100%) create mode 100644 src/app/ts43/token/route.ts rename {showcase-next/src => src}/app/user/info/page.tsx (100%) rename {showcase-next/src => src}/components/ui/alert.tsx (100%) rename {showcase-next/src => src}/components/ui/button.tsx (66%) rename {showcase-next/src => src}/components/ui/card.tsx (100%) rename {showcase-next/src => src}/components/ui/dialog.tsx (100%) rename {showcase-next/src => src}/components/ui/input.tsx (100%) rename {showcase-next/src => src}/components/ui/label.tsx (100%) rename {showcase-next/src => src}/components/ui/select.tsx (100%) rename {showcase-next/src => src}/components/ui/tabs.tsx (100%) create mode 100644 src/features/auth/auth-status.ts rename {showcase-next/src => src}/features/login/info-dialogs.tsx (93%) create mode 100644 src/features/login/legacy-showcase.css rename {showcase-next/src => src}/features/login/login-error.tsx (52%) create mode 100644 src/features/login/pending-authentication.tsx rename {showcase-next/src => src}/features/login/server-select.tsx (100%) create mode 100644 src/features/login/showcase-layout.tsx create mode 100644 src/features/login/showcase.tsx rename {showcase-next/src => src}/features/login/use-preferences.tsx (91%) create mode 100644 src/features/login/use-showcase-browser.ts create mode 100644 src/features/login/use-verification.ts rename {showcase-next/src => src}/features/phone/phone-input.tsx (73%) create mode 100644 src/features/qr/qr-dialog.tsx create mode 100644 src/features/qr/use-auth-polling.tsx create mode 100644 src/features/results/copy-json.tsx create mode 100644 src/features/results/result-view.tsx rename {showcase-next/src => src}/features/sms/sms-dialog.tsx (97%) rename {showcase-next/src => src}/features/sms/use-sms-flow.tsx (94%) rename {showcase-next/src => src}/features/sms/use-web-otp.tsx (100%) rename {showcase-next/src => src}/features/ts43/use-ts43-flow.tsx (91%) rename {showcase-next/src => src}/lib/utils.ts (100%) rename {showcase-next/src => src}/proxy.ts (83%) create mode 100644 src/server/auth/attempts.ts rename {showcase-next/src => src}/server/auth/failure.ts (77%) rename {showcase-next/src => src}/server/auth/input.ts (82%) rename {showcase-next/src => src}/server/auth/mobile.ts (79%) rename {showcase-next/src => src}/server/auth/provider.ts (76%) rename {showcase-next/src => src}/server/auth/repositories.ts (100%) create mode 100644 src/server/auth/service.ts create mode 100644 src/server/auth/state-cookies.ts create mode 100644 src/server/auth/web.ts create mode 100644 src/server/ciba/handlers.ts rename {showcase-next/src => src}/server/ciba/input.ts (100%) create mode 100644 src/server/ciba/provider.ts create mode 100644 src/server/ciba/service.ts rename {showcase-next/src => src}/server/config/load.ts (100%) create mode 100644 src/server/config/public.ts rename {showcase-next/src => src}/server/http/body.ts (100%) rename {showcase-next/src => src}/server/http/context.ts (87%) rename {showcase-next/src => src}/server/http/headers.ts (100%) rename {showcase-next/src => src}/server/http/response.ts (100%) rename {showcase-next/src => src}/server/integrity/attempts.ts (100%) rename {showcase-next/src => src}/server/integrity/handlers.ts (100%) rename {showcase-next/src => src}/server/integrity/input.ts (74%) rename {showcase-next/src => src}/server/integrity/ports.ts (100%) rename {showcase-next/src => src}/server/integrity/runtime.ts (100%) create mode 100644 src/server/integrity/service.ts rename {showcase-next/src => src}/server/integrity/verifier.ts (100%) rename {showcase-next/src => src}/server/session/cookies.ts (100%) rename {showcase-next/src => src}/server/session/store.ts (100%) rename {showcase-next/src => src}/server/shared/canonical-json.ts (100%) rename {showcase-next/src => src}/server/shared/context.ts (100%) rename {showcase-next/src => src}/server/shared/errors.ts (74%) create mode 100644 src/server/shared/http-client.ts rename {showcase-next/src => src}/server/shared/logger.ts (100%) rename {showcase-next/src => src}/server/shared/phone-number.ts (100%) rename {showcase-next/src => src}/server/store/auth-results.ts (90%) rename {showcase-next/src => src}/server/store/integrity.ts (100%) rename {showcase-next/src => src}/server/store/rate-limit.ts (100%) rename {showcase-next/src => src}/server/store/redis.ts (100%) rename {showcase-next/src => src}/shared/auth-failures.ts (62%) rename {showcase-next/src => src}/shared/contracts.ts (95%) rename {showcase-next/src => src}/shared/flow-catalog.ts (100%) rename {showcase-next/tests => tests}/contracts/auth-error.test.ts (84%) create mode 100644 tests/contracts/bootstrap.test.ts rename {showcase-next/tests => tests}/contracts/ciba.test.ts (63%) rename {showcase-next/tests => tests}/contracts/config-geoip.test.ts (93%) rename {showcase-next/tests => tests}/contracts/integrity.test.ts (98%) rename {showcase-next/tests => tests}/contracts/mobile-s2s.test.ts (53%) create mode 100644 tests/contracts/oauth-state.test.ts create mode 100644 tests/contracts/parity.test.ts create mode 100644 tests/contracts/production-origin.test.ts create mode 100644 tests/contracts/web-auth.test.ts create mode 100644 tests/helpers/auth-attempt.ts rename {showcase-next/tests => tests}/helpers/config.ts (93%) rename {showcase-next/tests => tests}/helpers/e2e-server.mjs (84%) rename {showcase-next/tests => tests}/helpers/processes.ts (100%) rename {showcase-next/tests => tests}/helpers/provider.ts (80%) rename {showcase-next/tests => tests}/helpers/redis.ts (100%) rename {showcase-next/tests => tests}/helpers/server-only.ts (100%) rename {showcase-next/tests => tests}/helpers/service-context.ts (100%) rename {showcase-next/tests => tests}/integration/auth-results.test.ts (100%) rename {showcase-next/tests => tests}/integration/config-runtime.test.ts (95%) rename {showcase-next/tests => tests}/integration/cross-instance.test.ts (59%) rename {showcase-next/tests => tests}/integration/integrity-concurrency.test.ts (100%) rename {showcase-next/tests => tests}/integration/production-runtime.test.ts (98%) rename {showcase-next/tests => tests}/integration/rate-limit.test.ts (100%) rename {showcase-next/tests => tests}/integration/sessions.test.ts (100%) create mode 100644 tests/services/auth.test.ts rename {showcase-next/tests => tests}/services/ciba.test.ts (79%) rename {showcase-next/tests => tests}/services/integrity.test.ts (70%) rename {showcase-next/tests => tests}/unit/auth-failure.test.ts (100%) rename {showcase-next/tests => tests}/unit/auth-results.test.ts (100%) create mode 100644 tests/unit/auth-status.test.ts rename {showcase-next/tests => tests}/unit/canonical-json.test.ts (100%) rename {showcase-next/tests => tests}/unit/config.test.ts (97%) rename {showcase-next/tests => tests}/unit/context.test.ts (88%) rename {showcase-next/tests => tests}/unit/integrity-verifier.test.ts (100%) rename {showcase-next/tests => tests}/unit/provider-error.test.ts (100%) create mode 100644 tests/unit/provider-timeout.test.ts create mode 100644 tests/unit/public-callbacks.test.ts create mode 100644 tests/unit/token-response.test.ts rename showcase-next/tsconfig.json => tsconfig.json (56%) rename showcase-next/vitest.config.ts => vitest.config.ts (100%) diff --git a/.dockerignore b/.dockerignore old mode 100755 new mode 100644 index 756f3dc..2e6abc1 --- a/.dockerignore +++ b/.dockerignore @@ -1,9 +1,18 @@ -.git -.gitignore node_modules -package-lock.json -docker-compose.yml -Dockerfile -.tool-versions -.dockerignore -README.md +.next +.git +.env +.env.* +!.env.example +config/default.json +config/local.json +config/local-*.json +config/stage.json +config/production.json +tests +e2e +test-results +playwright-report +*.tsbuildinfo +.local-backups +.superpowers diff --git a/showcase-next/.env.example b/.env.example similarity index 53% rename from showcase-next/.env.example rename to .env.example index a78272e..c50eae0 100644 --- a/showcase-next/.env.example +++ b/.env.example @@ -1,8 +1,11 @@ PORT=3002 APP_ENV=development -APP_ORIGIN=http://localhost:3002 REDIS_URL=redis://127.0.0.1:6380 SHOWCASE_KEY_PREFIX=showcase-next:development: +# Use 1 behind a single trusted Kubernetes ingress / reverse proxy. TRUST_PROXY_HOPS=0 # NODE_CONFIG_DIR=/absolute/path/to/private/config # PLAY_INTEGRITY_ENABLED=false + +# Provider request deadline in milliseconds (1..120000; invalid values use 15000). +PROVIDER_HTTP_TIMEOUT_MS=15000 diff --git a/.github/workflows/showcase-next-ci.yml b/.github/workflows/showcase-next-ci.yml new file mode 100644 index 0000000..fcf95a3 --- /dev/null +++ b/.github/workflows/showcase-next-ci.yml @@ -0,0 +1,62 @@ +name: Showcase Next.js CI + +on: + push: + pull_request: + +permissions: + contents: read + +concurrency: + group: showcase-next-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + verify: + name: Build and verify main flows + runs-on: ubuntu-latest + timeout-minutes: 20 + env: + CI: 'true' + NEXT_TELEMETRY_DISABLED: '1' + steps: + - name: Check out code + uses: actions/checkout@v6 + - name: Set up Node.js + uses: actions/setup-node@v7 + with: + node-version: '24.22.0' + cache: npm + cache-dependency-path: package-lock.json + - name: Install Redis + run: | + sudo apt-get update + sudo apt-get install -y redis-server + - name: Install Next.js dependencies + run: npm ci + - name: Install Chromium + run: npx playwright install --with-deps chromium + - name: Lint + run: npm run lint + - name: Typecheck + run: npm run typecheck + - name: Build + run: npm run build + - name: Unit and API tests + run: npm test + - name: Playwright main flows + run: >- + npm run test:e2e -- + e2e/main-flows.spec.ts + e2e/qr.spec.ts + e2e/sms.spec.ts + e2e/result.spec.ts + e2e/auth-error.spec.ts + - name: Upload Playwright diagnostics + if: failure() + uses: actions/upload-artifact@v4 + with: + name: showcase-next-playwright-results + path: test-results/ + if-no-files-found: ignore + retention-days: 7 diff --git a/.gitignore b/.gitignore index e18c744..6ade84e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,15 @@ -node_modules +node_modules/ +.next/ +.env* +!.env.example config/default.json -yarn.lock -public/css/style.css +config/local.json +config/local-*.json +config/stage.json +config/production.json +*.tsbuildinfo +test-results/ +playwright-report/ +.local-backups/ .DS_Store -.env -.cursor - +.superpowers/ diff --git a/Dockerfile b/Dockerfile index ef7e8cd..bcefd19 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,29 @@ -FROM node:24-alpine - +FROM node:24.22.0-bookworm-slim AS dependencies WORKDIR /usr/app -COPY package.json ./ -RUN npm install --only=prod +COPY package.json package-lock.json ./ +RUN npm ci +FROM dependencies AS builder COPY . . +ENV NEXT_TELEMETRY_DISABLED=1 +RUN npm run build -ENV PORT 8080 +FROM node:24.22.0-bookworm-slim AS production-dependencies +WORKDIR /usr/app +COPY package.json package-lock.json ./ +RUN npm ci --omit=dev && npm cache clean --force -EXPOSE 8080 -CMD [ "npm", "run", "start" ] +FROM node:24.22.0-bookworm-slim AS runtime +WORKDIR /usr/app +ENV NODE_ENV=production PORT=3002 NEXT_TELEMETRY_DISABLED=1 +COPY --from=production-dependencies /usr/app/node_modules ./node_modules +COPY --from=builder /usr/app/.next ./.next +COPY --from=builder /usr/app/public ./public +COPY --from=builder /usr/app/config ./config +COPY --from=builder /usr/app/runtime ./runtime +COPY --from=builder /usr/app/scripts ./scripts +COPY --from=builder /usr/app/server.mjs /usr/app/package.json /usr/app/next.config.ts ./ +RUN mkdir -p /run/showcase-config && chown -R node:node /usr/app /run/showcase-config +USER node +EXPOSE 3002 +CMD ["node", "server.mjs"] diff --git a/README.md b/README.md index f3ff3c5..262b912 100644 --- a/README.md +++ b/README.md @@ -1,93 +1,26 @@ # IPification Showcase -> A separate Next.js application is being migrated in `showcase-next/`. See the [migration guide](docs/NEXTJS_MIGRATION.md) for its runtime configuration, commands, compatibility limits and verification. The existing Express application remains available. +A Next.js demonstration of IPification phone verification, login and identity flows. +## Start locally -A demonstration application showcasing IPification's identity verification and authentication features. - -## Features - -- **Phone Number Verification** (IP, IP+, IM, SIM) -- **Quick Access Login** (IP, IP+, IM, SIM) -- **Identity Services** (Anonymous Identity, KYC) -- **QR Code Desktop Flow** -- **Server-to-Server Backchannel** -- **TS43 Protocol Support** - -## Local Development - -Choose either Node.js or Docker Compose to run the application locally. - -### Prerequisites - -- IPification account credentials -- For Node.js: Node.js >= 24.0.0 and npm (24.22.0 is pinned in `.tool-versions` and Docker) -- For Docker Compose: Docker with the Compose plugin - -### Run with Node.js - -Install dependencies and create the local configuration: +Use Node.js 24.22.0, npm, Redis and your IPification configuration: ```bash -npm install +npm ci cp config/default.json.sample config/default.json -``` - -Edit `config/default.json` with your IPification credentials, then start the server with auto-reload: - -```bash +cp .env.example .env.local +# Configure credentials in config/default.json and REDIS_URL in .env.local. +npm run config:check npm run dev ``` -Open `http://localhost:3000` (unless `PORT` is overridden in your environment or `.env`). To run without auto-reload, use `npm start`. +For phone QR testing, open one of the Network URLs printed at startup on the desktop and use a phone that can reach that address. Register the actual callback URL with your provider. -### Run with Docker Compose - -Create the configuration file mounted by Compose: +Run with Docker Compose from the repository root after configuring `config/default.json`: ```bash -cp config/default.json.sample config/default.json +docker compose up --build ``` -Edit `config/default.json` with your IPification credentials, then build and start the container: - -```bash -docker compose up -d --build -``` - -Open `http://localhost:3001`. Compose sets `PORT=3001` and mounts `config/default.json` as `/usr/app/config/default.json`. This container runs `npm start` without auto-reload; rebuild after application code changes. - -View logs or stop the local container: - -```bash -docker compose logs -f app -docker compose down -``` - -### Configuration - -See the [Quick Start Guide](docs/QUICKSTART.md) for setup and troubleshooting, and the [Sample Config Reference](docs/CONFIG_DEFAULT_JSON_SAMPLE.md) for configuration fields. - -The application loads `.env` for local environment overrides. Docker Compose sets the container port explicitly in `docker-compose.yml`. - -| Variable | Description | Default | -| ---------- | -------------------------------------------------- | ---------------------------- | -| `PORT` | Server port | `3000` (`3001` with Compose) | -| `NODE_ENV` | Environment (`development`, `stage`, `production`) | `development` | - -When testing from a mobile device, replace `localhost` with your computer's reachable IP address and configure the corresponding callback URL in your IPification client settings. - -## Documentation - -- **[Quick Start](docs/QUICKSTART.md)** - Setup guide and common issues -- **[API Reference](docs/API.md)** - Complete endpoint documentation -- **[Architecture](docs/ARCHITECTURE.md)** - System design and components -- **[PNV via IP](docs/PNV.md)** - Phone input, authorization redirect, callback, code exchange, and verification-result handling -- **[Sample Config Reference](docs/CONFIG_DEFAULT_JSON_SAMPLE.md)** - Application configuration fields -- **[Play Integrity backend demo](docs/PLAY_INTEGRITY.md)** - Backend verification setup, request hashing, and Android checklist -- **[Documentation Index](docs/INDEX.md)** - Documentation overview - -## Support - -- Developer Portal: https://developer.ipification.com/ -- Website: https://www.ipification.com/ +See the [integration guide](docs/nextjs/README.md) and [local setup and testing](docs/nextjs/running.md). The Playwright tests use a local mock provider and do not verify live stage or carrier behavior. diff --git a/showcase-next/components.json b/components.json similarity index 100% rename from showcase-next/components.json rename to components.json diff --git a/showcase-next/config/app_config.json b/config/app_config.json similarity index 100% rename from showcase-next/config/app_config.json rename to config/app_config.json diff --git a/showcase-next/config/default.json.sample b/config/default.json.sample similarity index 100% rename from showcase-next/config/default.json.sample rename to config/default.json.sample diff --git a/showcase-next/config/locale.json b/config/locale.json similarity index 100% rename from showcase-next/config/locale.json rename to config/locale.json diff --git a/docker-compose.next.yml b/docker-compose.next.yml deleted file mode 100644 index e3f68b9..0000000 --- a/docker-compose.next.yml +++ /dev/null @@ -1,51 +0,0 @@ -services: - showcase-next: - image: ipification_showcase_next:local - build: - context: ./showcase-next - restart: unless-stopped - init: true - environment: - NODE_ENV: production - APP_ENV: ${SHOWCASE_APP_ENV:-stage} - NODE_CONFIG_ENV: ${SHOWCASE_APP_ENV:-stage} - NODE_CONFIG_DIR: /run/showcase-config - APP_ORIGIN: ${SHOWCASE_APP_ORIGIN:-http://localhost:3002} - REDIS_URL: redis://showcase-redis:6379 - SHOWCASE_KEY_PREFIX: 'showcase-next:${SHOWCASE_APP_ENV:-stage}:' - TRUST_PROXY_HOPS: ${SHOWCASE_TRUST_PROXY_HOPS:-0} - PORT: 3002 - ports: - - '3002:3002' - volumes: - - type: bind - source: ./config/default.json - target: /run/showcase-config/default.json - read_only: true - bind: - create_host_path: false - depends_on: - showcase-redis: - condition: service_healthy - healthcheck: - test: ['CMD', 'node', '-e', "fetch('http://127.0.0.1:3002/health/ready').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"] - interval: 15s - timeout: 5s - retries: 3 - start_period: 30s - stop_grace_period: 15s - showcase-redis: - image: redis:7.4.7-alpine - restart: unless-stopped - command: ['redis-server', '--appendonly', 'yes'] - ports: - - '127.0.0.1:6380:6379' - volumes: - - showcase-next-redis:/data - healthcheck: - test: ['CMD', 'redis-cli', 'ping'] - interval: 5s - timeout: 3s - retries: 5 -volumes: - showcase-next-redis: diff --git a/docker-compose.yml b/docker-compose.yml index 96f2dcf..3720de0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,12 +1,50 @@ services: - app: - restart: unless-stopped + showcase-next: + image: ipification_showcase_next:local build: context: . - image: "ipification_showcase" + restart: unless-stopped + init: true environment: - PORT: 3001 + NODE_ENV: production + APP_ENV: ${SHOWCASE_APP_ENV:-stage} + NODE_CONFIG_ENV: ${SHOWCASE_APP_ENV:-stage} + NODE_CONFIG_DIR: /run/showcase-config + REDIS_URL: redis://showcase-redis:6379 + SHOWCASE_KEY_PREFIX: 'showcase-next:${SHOWCASE_APP_ENV:-stage}:' + TRUST_PROXY_HOPS: ${SHOWCASE_TRUST_PROXY_HOPS:-0} + PORT: 3002 + ports: + - '3002:3002' + volumes: + - type: bind + source: ./config/default.json + target: /run/showcase-config/default.json + read_only: true + bind: + create_host_path: false + depends_on: + showcase-redis: + condition: service_healthy + healthcheck: + test: ['CMD', 'node', '-e', "fetch('http://127.0.0.1:3002/health/ready').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"] + interval: 15s + timeout: 5s + retries: 3 + start_period: 30s + stop_grace_period: 15s + showcase-redis: + image: redis:7.4.7-alpine + restart: unless-stopped + command: ['redis-server', '--appendonly', 'yes'] ports: - - 3001:3001 + - '127.0.0.1:6380:6379' volumes: - - ./config/default.json:/usr/app/config/default.json + - showcase-next-redis:/data + healthcheck: + test: ['CMD', 'redis-cli', 'ping'] + interval: 5s + timeout: 3s + retries: 5 +volumes: + showcase-next-redis: diff --git a/docs/INDEX.md b/docs/INDEX.md index 4fc8558..44655f3 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -1,330 +1,5 @@ -# Documentation Index +# Documentation -> A separate Next.js application is being migrated in `showcase-next/`. See the [migration guide](NEXTJS_MIGRATION.md) for its runtime configuration, commands, compatibility limits and verification. The existing Express application remains available. +Start with the [integration guide](nextjs/README.md) for the current Next.js application. It includes browser, QR, SMS and SIM flows, endpoint reference, local setup, and test instructions. - -Welcome! Find what you need quickly. - -## 🚀 Start Here - -**New to the project?** -→ [Quick Start Guide](QUICKSTART.md) - Be running in 5 minutes - -**Want to understand the project?** -→ [README.md](../README.md) - Complete overview - -**Need to configure something?** -→ [Configuration Guide](CONFIG_DEFAULT_JSON_SAMPLE.md) - All config fields explained - ---- - -## 📚 Documentation by Goal - -### I want to... - -**...set up the project quickly** -→ [Quick Start](QUICKSTART.md) - 5-minute setup - -**...configure auth servers** -→ [Config Guide - auth_servers](CONFIG_DEFAULT_JSON_SAMPLE.md#auth_servers-fields) - -**...customize UI text** -→ [Locale Guide](LOCALE.md) - Text labels and translations - -**...understand authentication flows** -→ [Architecture](ARCHITECTURE.md) - How authentication works - -**...follow phone verification via IP end to end** - -→ [PNV via IP](PNV.md) - Phone input, redirects, code exchange, userinfo, and current verification limitations - -**...integrate with the API** -→ [API Reference](API.md) - Endpoint documentation - -**...use SIM verification (TS43)** -→ [TS43 Guide](TS43.md) - SIM-based auth - -**...use SMS OTP verification** -→ [SMS Flow Guide](SMS.md) - SMS-based phone verification - -**...customize app behavior** -→ [App Config](APP_CONFIG_JSON.md) - UI and versioning settings - ---- - -## 📖 All Documentation Files - -### User Guides - -- **[Quick Start](QUICKSTART.md)** - Fast setup with examples -- **[Configuration](CONFIG_DEFAULT_JSON_SAMPLE.md)** - All config fields -- **[Locale](LOCALE.md)** - Customize UI text - -### Feature Guides - -- **[PNV via IP](PNV.md)** - End-to-end IP verification flow with code references and examples -- **[TS43 SIM Verification](TS43.md)** - Use SIM-based auth -- **[SMS Verification Flow](SMS.md)** - Use SMS OTP for phone verification -- **[Multi-Server Setup](CONFIG_DEFAULT_JSON_SAMPLE.md#auth_servers-fields)** - Configure multiple auth servers - -### Reference - -- **[API](API.md)** - Endpoint reference -- **[Architecture](ARCHITECTURE.md)** - System overview -- **[App Config](APP_CONFIG_JSON.md)** - App settings - ---- - -## Documentation Structure - -``` -docs/ -├── INDEX.md ← You are here -├── QUICKSTART.md ← Start here for setup -├── CONFIG_DEFAULT_JSON_SAMPLE.md ← Config reference -├── LOCALE.md ← UI text customization -├── PNV.md ← IP phone verification flow and code walkthrough -├── TS43.md ← SIM verification guide -├── SMS.md ← SMS OTP verification guide -├── API.md ← API endpoint reference -├── ARCHITECTURE.md ← System overview -└── APP_CONFIG_JSON.md ← App behavior settings -``` - ---- - -## Need Help? - -### Common Questions - -**Q: How do I start the app?** -A: See [Quick Start](QUICKSTART.md) - -**Q: How do I add another auth server?** -A: Edit `auth_servers` array in config - [Guide](CONFIG_DEFAULT_JSON_SAMPLE.md#auth_servers-fields) - -**Q: How do I change button labels?** -A: Edit `locale` in config - [Guide](LOCALE.md) - -**Q: How do I enable SIM verification?** -A: Add `pvn_sim` to clients - [TS43 Guide](TS43.md) - -**Q: How do I enable SMS OTP verification?** -A: Add `pvn_sms` to clients - [SMS Flow Guide](SMS.md) - -**Q: Where are the API endpoints?** -A: See [API Documentation](API.md) - -### Resources - -- **IPification Developer Portal**: https://developer.ipification.com/ -- **IPification Website**: https://www.ipification.com/ - ---- - -## File Descriptions - -
-Click to see detailed description of each documentation file - -### Quick Start Guide - -**File**: `docs/QUICKSTART.md` - -**Contents**: - -- 5-minute setup instructions -- Prerequisites checklist -- Common issues and solutions -- Testing different flows - -**Best for**: First-time users, quick setup - ---- - -### Main README - -**File**: `README.md` (root directory) - -**Contents**: - -- Project overview -- Features list -- Installation instructions -- Configuration guide -- User flows explanation -- API endpoints overview -- Docker deployment -- Development guide -- Troubleshooting - -**Best for**: Complete reference, understanding the project - ---- - -### API Documentation - -**File**: `docs/API.md` - -**Contents**: - -- Detailed endpoint documentation -- Request/response formats -- Query parameters -- Status codes -- WebSocket events -- Error handling -- Code examples - -**Best for**: API integration, endpoint reference - ---- - -### Architecture Documentation - -**File**: `docs/ARCHITECTURE.md` - -**Contents**: - -- System architecture diagrams -- Component structure -- Data flow diagrams -- Security architecture -- Module structure -- Performance considerations -- Extension points - -**Best for**: Understanding system design, extending the application - ---- - -### TS43 Route Documentation - -**File**: `docs/TS43.md` - -**Contents**: - -- TS43 router responsibilities and flow -- Endpoint behavior and request/response fields -- Helper function roles -- Key code snippet for digital request output - -**Best for**: Understanding TS43 flow and router behavior - ---- - -### SMS Flow Documentation - -**File**: `docs/SMS.md` - -**Contents**: - -- SMS OTP verification flow (CIBA with channel sms) -- Quick setup and configuration -- Frontend integration (login button, start_pvn_sms, start_sms_flow) -- API endpoints: `/sms/auth`, `/sms/token`, `/sms/log` -- Backend → auth server: CIBA auth, SMS callback, token exchange -- Troubleshooting - -**Best for**: Implementing or debugging SMS phone verification - ---- - -### Sample Config Reference - -**File**: `docs/CONFIG_DEFAULT_JSON_SAMPLE.md` - -**Contents**: - -- How to copy and apply the sample config -- Required vs optional configuration fields -- Auth servers configuration (multi-server support) -- Client flow configuration with credential override mechanism -- Locale override fields -- Configuration examples and troubleshooting - -**Best for**: Understanding and customizing `config/default.json.sample` - -**Key Features**: - -- Multi auth server support with dropdown selection -- Credential fallback mechanism (root vs per-client) -- Complete field validation on startup - ---- - -### App Config Reference - -**File**: `docs/APP_CONFIG_JSON.md` - -**Contents**: - -- App UI configuration schema -- Versioning and maintenance settings -- Branding, environment labels, and screen text -- Override examples via `config/default.json` - -**Best for**: Customizing `config/app_config.json` and app UI behavior - ---- - -### Locale Configuration - -**File**: `docs/LOCALE.md` - -**Contents**: - -- Locale file structure -- Override mechanism -- Deep merging behavior -- Usage examples -- Troubleshooting guide - -**Best for**: Customizing UI text labels, internationalization setup - -## Documentation Conventions - -### Code Examples - -All code examples use: - -- **bash** for shell commands -- **json** for configuration files -- **javascript** for code snippets - -### Diagrams - -Architecture diagrams use ASCII art format for universal compatibility. - -### Links - -- Internal links use relative paths -- External links open in new tabs (when applicable) - -## Contributing to Documentation - -When updating documentation: - -1. Keep examples up-to-date with code changes -2. Update all affected documentation files -3. Test code examples before committing -4. Follow existing documentation style -5. Update this index if adding new documentation files - -## Documentation Updates - -**Last Updated**: [Current Date] - -**Version**: 1.1.27-SNAPSHOT - -**Maintainer**: Development Team - -## Feedback - -For documentation feedback or improvements: - -- Create an issue in the repository -- Submit a pull request with improvements -- Contact the development team - -
+The `NEXTJS_*` migration notes and Play Integrity documents record earlier implementation decisions; verify historical material against the current code. diff --git a/docs/NEXTJS_AUTH_FLOWS.md b/docs/NEXTJS_AUTH_FLOWS.md new file mode 100644 index 0000000..d377e75 --- /dev/null +++ b/docs/NEXTJS_AUTH_FLOWS.md @@ -0,0 +1,171 @@ +# Reading the authentication demo + +Start with the method you want to integrate. The running demo uses the same frontend entry points described here; there is no separate simplified implementation to keep in sync. + +## Where to start + +1. [Showcase screen](../src/features/login/showcase.tsx): find `onClick={() => verification.startVerification(client)}`. The selected client comes from configuration. +2. [Verification entry points](../src/features/login/use-verification.ts): `startVerification` validates the selection, prevents duplicate activation, and selects the named method below. +3. Follow that method's request and callback path to [the result page](../src/app/user/info/page.tsx). + +| Method | Function in `use-verification.ts` | Continue reading | +| --- | --- | --- | +| IP / IP+ | `startIpVerification` | `openBrowserVerification`, then backend `/auth/start` | +| IM | `startMessagingVerification` | `openBrowserVerification`, always direct navigation | +| SMS | `startSmsVerification` | [SMS dialog](../src/features/sms/sms-dialog.tsx) and [SMS requests](../src/features/sms/use-sms-flow.tsx) | +| SIM / TS43 | `startSimVerification` | [SIM requests and browser credential API](../src/features/ts43/use-ts43-flow.tsx) | +| Anonymous Identity / KYC | `startIdentityVerification` | `openBrowserVerification`; KYC includes the phone, Anonymous Identity omits it | + +IP+ uses the same browser entry point as IP; the configured client/scope distinguishes the provider request. IP via App also uses that entry point, with its button offered only on mobile. + +## Backend entry points + +For the web flow, the [HTTP handlers](../src/server/auth/web.ts) and [application service](../src/server/auth/service.ts) use matching operation names. Start at one of these endpoints: + +| Endpoint | Function to find | Responsibility | +| --- | --- | --- | +| `POST /auth/prepare` | `prepareWebAuthentication` | Create a browser-bound attempt (QR: two minutes to scan; direct: five minutes) and return its start URL | +| `GET /auth/status` | `getAuthenticationStatus` | Owner-bound recovery; exact-state authenticated session can report finished | +| `GET /auth/start` | `startWebAuthentication` | Select the configured client/server and build the provider authorization redirect | +| `GET /auth/callback/:userFlow/:serverId` | `handleAuthorizationCallback` | Exchange the code, save results, then return a redirect or QR/backchannel outcome | +| `GET /auth/complete` | `completeWebAuthentication` | Wait for the saved result and establish the browser's demo display session | +| `POST /auth/mobile/login` | `exchangeMobileCode` | Exchange the mobile app's code and submitted redirect URI, then return user info as JSON | +| `POST /auth/s2s/signin` | `readS2sResult` | Read the raw result previously cached by a callback using its `state` | + +Mobile HTTP response handling lives in [auth/mobile.ts](../src/server/auth/mobile.ts). This API creates no browser session. The existing response precedence allows a provider's `auth_server` field to take precedence over the locally selected server; provider failures retain this endpoint's HTTP 401 mapping. + +[S2S signin](../src/app/auth/s2s/signin/route.ts) performs a non-consuming cache read, so callers can read the same stored result again while it remains available. It does not exchange a new authorization code or create a browser session; a cache miss returns an empty HTTP 401 response. + +## Public configuration and country lookup + +- [GET /api/config](../src/app/api/config/route.ts): `getPublicConfig` resolves the request context, then calls [toPublicConfig](../src/server/config/public.ts) to project the public response fields. Client secrets remain on the server. `redirect_uri` includes the default server id; `redirect_uris[serverId]` gives the matching callback for each configured server. +- [GET /api/geoip](../src/app/api/geoip/route.ts): `getClientCountry` uses the client IP resolved under the configured proxy policy. A GeoIP match wins; a miss or lookup failure keeps `default_country_code`, or `rs` when no default exists. The response returns the lowercase country and the selected IP. + +These short handlers stay in their route files so customers can read the complete operation without another service layer. + +## IP, IP+, IM, and Identity + +`openBrowserVerification` keeps the browser flow together: + +1. POST `/auth/prepare` with `user_flow`, `server_id`, `qr` and the normalized `phone` only for methods that require it. +2. Use the returned server-generated `state` and `/auth/start` URL. The response sets a per-attempt HttpOnly owner cookie; keep it in the initiating browser. +3. Continue in the same browser on mobile, or show a QR on desktop. IM always navigates directly, including on desktop. + +For example, an IP phone-verification request has this shape (values below are placeholders): + +```text +/auth/start?user_flow=pvn_ip&server_id=stage&state=&phone= +``` + +For QR attempts, the backend appends `-qrcode` to the state. The QR points at this app's `/auth/start` URL, not directly at a provider token endpoint. + +The backend sequence is in [web handlers](../src/server/auth/web.ts), [authentication service](../src/server/auth/service.ts), and [provider requests](../src/server/auth/provider.ts): + +```text +POST /auth/prepare + -> /auth/start + -> provider authorization + -> /auth/callback/:userFlow/:serverId + -> exchange authorization code and obtain user info on the server + -> /auth/complete?state=... + -> session-backed /user/info +``` + +With QR, the phone and desktop have separate browser sessions. [QrDialog](../src/features/qr/qr-dialog.tsx) uses [HTTP status polling](../src/features/qr/use-auth-polling.tsx) immediately and every two seconds. The QR is shown after the first successful pending status response. After the phone callback persists its result and terminal state in Redis, the desktop reads the matching completion URL and visits `/auth/complete`, which establishes its own display session. Completion URLs must use this app's origin, `/auth/complete`, and the exact attempt state. No Socket.IO connection is used. + +Creating a new QR immediately disconnects the previous listener and cancels its status checks before preparing a new state. Preparation responses from cancelled/tab-switched attempts cannot reopen the dialog. QR links have two minutes to start from `/auth/prepare`; direct flows keep their five-minute attempt lifetime. The popup uses the prepare response's `expiresAt` for its scan countdown, including time spent preparing/loading. A successful first QR `/auth/start` atomically opens a separate five-minute completion window from that start. Owner-authenticated `/auth/status` returns `started` and the current `expiresAt`; the popup then hides the QR and displays a completion countdown. It keeps status recovery active across the scan deadline to handle a scan arriving just before expiry; the backend rejects an unstarted expired QR, and confirmed backend expiry stops recovery. Expired UI offers a new code. Visibility/focus updates recompute the remaining time after returning to a background tab. The provider can have its own expiry. + +The start endpoint binds a separate callback cookie to the scanning browser. Callback claims and owner completion are atomic and single-use in Redis, bound to flow/server/client/origin and the originally submitted phone. Missing, duplicate, expired or replayed states fail before provider calls. Owner-authenticated `/auth/status` checks report pending, completed or failed attempts, including provider failures. Scanner cookies cannot establish the desktop session. + +For a bound browser callback, a nonempty `error` is displayed first; if it is absent, `error_description` is displayed. When neither error parameter nor a nonempty `code` is present, the app displays `Unable to complete authentication.` No token exchange occurs for these cases. Messages are length-limited and common credential patterns and links are redacted; they are kept in the browser session (and the bound attempt for QR completion), not added to redirect URLs. The QR scanner and the browser that opened the QR receive their own session messages. State and callback binding checks still apply. + +Messaging backchannels may omit the callback cookie only for `pvn_im`/`login_im` with a matching started state. In this exception the unpredictable state is a bearer credential and the authorization code is checked by token exchange; the header alone is insufficient. Backchannels never adopt browser sessions. If the browser returns while the backchannel is exchanging, `/auth/pending` waits through owner-authenticated status checks; an already completed backchannel result is reused without another exchange. This is not independent authentication of the backchannel sender. + +Custom browser integrations must POST prepare first, retain cookies, and follow the returned URL. This does not alter the submitted-code mobile API or the separately signed Play Integrity protocol. PKCE and ID-token validation are outside this change. See [RFC 9700 section 4.7](https://www.rfc-editor.org/rfc/rfc9700.html#section-4.7) for user-agent binding context. + +## SMS: request code, submit code, show result + +Read [useSmsFlow](../src/features/sms/use-sms-flow.tsx) from `start` to `submit`: + +1. `POST /sms/auth`: send the selected `client_id`, `server_id`, normalized phone as `login_hint`, and scope. +2. Keep `auth_req_id` and `nonce` returned by the backend. [SmsDialog](../src/features/sms/sms-dialog.tsx) lets the user enter the OTP; [WebOTP](../src/features/sms/use-web-otp.tsx) can supply it on a supported browser. +3. `POST /sms/token`: submit the OTP together with the original request identifiers and client/server selection. +4. The existing demo logs the token response through `/sms/log`, then navigates to `/user/info` after a successful response. Wrong codes remain retryable in the dialog. + +The [CIBA backend service](../src/server/ciba/service.ts) owns provider requests and session creation; client secrets never belong in React. + +On the backend, follow `sendSmsCode` from [the HTTP handler](../src/server/ciba/handlers.ts) through the service to [the provider client](../src/server/ciba/provider.ts). It posts to `/ext/ciba/auth` with `channel=sms` and the configured message. Follow `verifySmsCode` through the same files to see OTP submission to `/ext/bc/sms/callback`, then `/token` with the CIBA grant, then `/userinfo`. + +The callback submission is best-effort in the existing integration: even if it fails, the backend still attempts the token request. The token response determines the API outcome. The service creates a **demo display session** for both successful user info and error data, so a session alone does not prove that phone verification succeeded. + +## SIM: request credential, obtain it, exchange it + +Read [useTs43Flow](../src/features/ts43/use-ts43-flow.tsx) in its numbered order: + +1. `POST /ts43/auth`: obtain `digital_request`, the request id, and nonce from the backend. +2. `navigator.credentials.get(...)`: ask a supported device/browser for the SIM credential. +3. `POST /ts43/token`: send the returned `vp_token`, request id, client id, nonce, and the same selected `server_id` to the backend. +4. The existing demo sends `/ts43/log`, then opens `/user/info` to display the session-backed response. + +This path requires a supported device/browser; a desktop fake-provider test cannot prove real-device SIM support. The token request now preserves the auth request's selected server. + +The backend entry points are `createSimCredentialRequest` and `exchangeSimCredential` in [handlers](../src/server/ciba/handlers.ts), [service](../src/server/ciba/service.ts), and [provider](../src/server/ciba/provider.ts). The first calls `/ext/ciba/auth`, then `/ext/bc/ts43/dcql`. The second submits `vp_token` to `/ext/bc/ts43/callback`, then exchanges the CIBA request at `/token` and fetches `/userinfo`. The callback and demo-session rules described for SMS also apply here. + +## Optional mobile Play Integrity API + +This is a backend/mobile flow; collecting the Android Integrity token is outside the React Showcase screen. Follow [the HTTP handler](../src/server/integrity/handlers.ts) to `execute` in [the Integrity service](../src/server/integrity/service.ts), then read its three named steps in order: + +| Endpoint | Named step | Input / result | +| --- | --- | --- | +| `POST /api/play-integrity/attempt` | `createAttempt` | `phone_number`, `server_id` → `attempt_id`, `request_hash`, expiry | +| `POST /api/play-integrity/verify` | `verifyAttempt` | `attempt_id`, `integrity_token` → signed `state`, expiry | +| `POST /api/play-integrity/token-exchange` | `exchangeCode` | authorization `code`, signed `state` → `user_info` | + +An attempt is claimed before verification; Google verification follows the configured incoming-hostname policy. A successful verification approves a transaction. Code exchange claims that transaction and uses its captured client credentials and redirect URI, then marks it consumed or failed. The [attempt implementation](../src/server/integrity/attempts.ts) and [Redis store](../src/server/store/integrity.ts) retain the single-use state transitions. The HTTP handler retains feature gating, its 16 KB request limit, and public error/status mapping. + +## What is only Showcase presentation? + +- [Showcase layout](../src/features/login/showcase-layout.tsx): logo, server selector, footer, About and consent UI. +- [Browser presentation](../src/features/login/use-showcase-browser.ts): mobile/desktop detection, country default via GeoIP, and keyboard-focus appearance. +- [Preferences](../src/features/login/use-preferences.tsx): selected tab/server and browser storage. +- [Phone input](../src/features/phone/phone-input.tsx): digits-only entry, country selection, number normalization, validation, and the stage-only Wonderland country. As in the legacy UI, typing is not capped at a country-specific valid length and nonempty input enables methods; `startVerification` still rejects invalid real-country numbers before any prepare/SMS/SIM request. +- [Result view](../src/features/results/result-view.tsx): full original userinfo JSON shown immediately, its copy button, and a short explanation of `phone_number_verified`. Boolean and string claim values are preserved, including false; extra/nested claims remain visible. + +When adapting the React flow, retain the guards around duplicate activation and cancellation: late async responses must not affect a newer attempt. These small guards are commented where they are used. + +The result page follows the legacy title → full userinfo → client id/state layout, with a short explanation below the JSON only when the original userinfo contains `phone_number_verified`. Its presence is checked explicitly, so boolean false still shows the explanation. It does not hide userinfo behind a disclosure or replace it with selected highlights. `true` / `"true"` indicates successful phone verification; `false` / `"false"` indicates it did not succeed, and missing/unknown values do not confirm it. The JSON and copied content keep the provider's original values and types. + +The result page is a demo display of a session-backed response. Apply your own application's acceptance policy when integrating verification; the existing migration deliberately preserves the behaviors documented in the [compatibility ledger](NEXTJS_MIGRATION.md#compatibility-ledger). For phone verification, see the explicit true-only rule in [PNV integration guidance](nextjs/browser-flows.md). + +## Small support endpoints + +- [GET /auth/login/error](../src/app/auth/login/error/route.ts) reads and clears the session's login error once, so it is not shown repeatedly after refresh. +- [GET /auth/session-required](../src/app/auth/session-required/route.ts) redirects to `/`; [GET /](../src/app/route.ts) redirects to `/auth/login`. +- [GET /health/live](../src/app/health/live/route.ts) reports liveness. [GET /health/ready](../src/app/health/ready/route.ts) requires valid config and a successful Redis ping. These are service probes, not verification steps. +- [POST /sms/log](../src/app/sms/log/route.ts) and [POST /ts43/log](../src/app/ts43/log/route.ts) retain the existing diagnostics handler: it reads the body and acknowledges it with `OK`; it does not authenticate a user or call the provider. + +## Checking your changes + +From the repository root, use `npm run lint`, `npm run typecheck`, `npm test`, `npm run test:integration`, then `npm run build` and `npm run test:e2e`. Browser tests use a fake provider and disposable Redis. In particular, `e2e/flow-routing.spec.ts` checks mobile IP, desktop IM, and Identity request parameters; the other suites check QR callbacks, OTP, SIM cancellation/retry, focus, and result display. + +## Malformed provider token responses + +Authorization-code and CIBA exchanges reject HTTP 200 token bodies that are null, arrays, or lack a nonempty string `access_token`. They stop before userinfo and classify friendly browser diagnostics as `TOKEN_RESPONSE_INVALID` at the token step. Raw responses and tokens are never added to these diagnostics. The existing SMS/SIM demo error-session behavior remains unchanged. + + +## Provider deadlines and status recovery + +IPification form and JSON requests have a 15-second deadline per request, including connection and response waits. `PROVIDER_HTTP_TIMEOUT_MS` accepts an integer from 1 to 120000; absent or invalid values use 15000. Requests are never automatically retried, including authorization-code exchange. A timeout is shown as `PROVIDER_TIMEOUT` with a safe explanation and an action to start a new verification. CIBA callback submission remains best-effort: after failure or timeout, the existing token → userinfo sequence continues once. Google Integrity retains its existing timeout; SMS/SIM fixed scopes and debug requests remain unchanged. + +QR and IM share status polling immediately and every 2000 ms, with one request at a time and a five-second browser request timeout. Network failures keep recovery active and show a connection message; IM offers Retry now. Polling stops on completion, confirmed expiry, local hard deadline or component cleanup. QR recovery is bounded by the original two-minute scan deadline plus the maximum five-minute completion window; a started response can shorten this to the authoritative completion deadline. IM waiting is bounded by five minutes and may expire earlier on a backend response. Completion URLs must belong to this origin, use `/auth/complete`, and contain the exact attempt state. A finished status opens `/user/info` without consuming completion again. + +After completion clears the owner cookie, `/auth/status` can return `finished` only when the current authenticated session's stored valid state exactly matches the requested state. This does not create or adopt another session, and all other states still require owner binding. + + +## Incoming origin + +Both development and production always detect the public origin from each request. Direct access uses the request host and protocol. With `TRUST_PROXY_HOPS=1` behind one trusted ingress, the forwarded host and protocol are used, so an HTTPS ingress can forward internal HTTP without producing HTTP callbacks. Ingress owns domain routing; no application domain list is needed. Keep the ingress forwarded-header handling and existing network access policy in place. + +QR URLs, public callback URLs, provider redirect URIs and Secure cookie selection follow this origin. Each authentication attempt captures its original origin; continue that attempt on the same domain. Each public callback URL still needs provider registration. For mobile QR testing, open the desktop app through its LAN IP or a shared domain: localhost in a QR points to the phone itself. + +`npm run dev` prints localhost and deduplicated LAN IPv4 login URLs with the configured port, plus the same-network QR instruction. With no LAN IPv4, it prints a connect/restart hint. Production startup does not print the LAN guide. diff --git a/docs/NEXTJS_BACKEND.md b/docs/NEXTJS_BACKEND.md index 37a1f5d..d6ad653 100644 --- a/docs/NEXTJS_BACKEND.md +++ b/docs/NEXTJS_BACKEND.md @@ -1,6 +1,8 @@ # Next.js backend structure -This is the application-service refactor of the [Next.js migration](NEXTJS_MIGRATION.md). It changes responsibility boundaries, not the authentication policy or public HTTP contracts. +> Historical reference: the application now lives at the repository root. Use the [current integration guide](nextjs/README.md) for setup and flows. + +This is the application-service refactor of the [Next.js migration](NEXTJS_MIGRATION.md). The initial refactor preserved behavior. The approved 2026-10-31 follow-up below adds explicit state/token validation and corrects callback metadata/server selection; PNV and demo display-session policy remain unchanged. ## Request flow @@ -8,20 +10,49 @@ This is the application-service refactor of the [Next.js migration](NEXTJS_MIGRA 2. Each feature's `input.ts` projects known request fields into a named DTO. Integrity retains its existing required-string/JWT-shape validation. General OAuth/CIBA fields remain `unknown` when their wire format belongs to the provider; server/client lookup keeps the previous strict-match/default behavior. Additional schema restrictions need separate compatibility approval. 3. `auth/service.ts`, `ciba/service.ts` and `integrity/service.ts` orchestrate use cases with plain data and discriminated outcomes. They do not import Next.js, accept a Request or construct a Response. Factories accept explicit provider/repository ports for failure and ordering tests; default instances are stateless. 4. Provider clients (`auth/provider.ts`, `ciba/provider.ts`, the shared HTTP client and Integrity verifier) own outbound protocol calls. `ProviderError` gives token/userinfo/authentication/DCQL failures a known step, message, optional upstream status and payload. HTTP adapters preserve each endpoint's existing presentation. Friendly callback pages only expose allowlisted diagnostic codes, never raw provider payloads. -5. `auth/repositories.ts` declares session/result ports and connects them to the existing Redis implementations in `session/store.ts` and `store/auth-results.ts`. `integrity/ports.ts` describes the attempt/transaction, verifier and code-exchange ports. `integrity/runtime.ts` composes environment configuration and concrete adapters. Atomic Redis transitions, TTLs and Socket.IO publishing remain in their existing implementations. +5. `auth/repositories.ts` declares session/result ports and connects them to the existing Redis implementations in `session/store.ts` and `store/auth-results.ts`. `integrity/ports.ts` describes the attempt/transaction, verifier and code-exchange ports. `integrity/runtime.ts` composes environment configuration and concrete adapters. Atomic Redis transitions and TTLs remain in their existing implementations; clients read completion through owner-authenticated HTTP status polling. `shared/context.ts` defines runtime facts required by services. `shared/errors.ts` provides target lookup, `ApplicationError`, `ProviderError` and `UpstreamUnavailableError`. There is no service locator or dependency-injection framework. +## Customer reading path + +For SMS and SIM, the HTTP handler, application service, and provider client now use the same operation names. Start at the route, then find that name in [handlers](../src/server/ciba/handlers.ts), [service](../src/server/ciba/service.ts), and [provider](../src/server/ciba/provider.ts): + +| Endpoint | Operation | Provider sequence | +| --- | --- | --- | +| `POST /sms/auth` | `sendSmsCode` | CIBA auth with SMS channel/message | +| `POST /sms/token` | `verifySmsCode` | SMS callback → token → userinfo | +| `POST /ts43/auth` | `createSimCredentialRequest` | CIBA auth → DCQL credential request | +| `POST /ts43/token` | `exchangeSimCredential` | SIM credential callback → token → userinfo | + +The named handlers share input decoding and HTTP response formatting. Only SIM auth runs its existing rate limiter, before client/server lookup. Auth's text client-not-found response and token's JSON client-not-found response remain distinct. The previously unused `ciba/sms.ts` and `ciba/ts43.ts` wrappers were removed. + +The provider's `submitCallback` is deliberately best-effort: a failed SMS/SIM callback still proceeds to the token request. Shared token/userinfo code retains that order and the existing error normalization. It adds no retries. The approved follow-up validates token bodies before userinfo. + +After token exchange, `exchangeAndCreateDemoSession` calls `createDemoDisplaySession` for either success or error data. Its `isAuthenticated` flag permits viewing the Showcase result page; it is not a production phone-verification acceptance policy. Internal success is named `token_exchanged` to describe what actually completed. The external response, cookie behavior, and error-session fallback are unchanged. + +[Integrity service](../src/server/integrity/service.ts) keeps three named steps in one file: `createAttempt`, `verifyAttempt`, and `exchangeCode`. The existing `execute` entry point validates input and awaits each step inside its outer exception boundary. Atomic claim/reject/approve/finalize operations remain in the repository; extracting functions does not move these boundaries. + +For the frontend-to-backend walkthrough and the fields used at each step, see [Reading the authentication demo](NEXTJS_AUTH_FLOWS.md). + +### Remaining web/mobile and support APIs + +Auth's real HTTP/service entry points now have matching names: `startWebAuthentication`, `handleAuthorizationCallback`, `completeWebAuthentication`, `exchangeMobileCode`, and `readS2sResult`. The service contains named functions in flow order rather than short generic object-method names. Provider URL construction keeps the existing parameters and makes the IP-via-App endpoint selection explicit. + +Public config, GeoIP, and S2S remain small route-local handlers. Their names explain the operation; no extra service/factory was added. The public config projection now includes default and per-server callback URLs; GeoIP still prefers a lookup match over its configured fallback; S2S still reads cached data without consuming it or setting a cookie. Mobile still uses the submitted redirect URI and returns JSON without adopting a browser session. + +Lightweight redirects, session-error consumption, diagnostics and health probes were reviewed and retain their small implementations. The customer guide lists their roles separately from authentication steps. Callback cancellation still preserves existing session data, and backchannel callbacks cannot create browser sessions; all web callbacks now claim validated prepared state before side effects. + ## Behavior that deliberately remains -- Resolve server/client before the missing-state redirect. Preserve duplicate query values rather than quietly taking the first. +- Resolve server/client with existing lookup defaults. Duplicate web state values are rejected; state cannot be synthesized on callback. - Write both display/raw results before notifying the QR room. A backchannel callback returns before the QR path. QR fails only for string `'false'`. -- Completion reads the result, then creates/replaces the session. Missing completion keeps five reads and five delays. S2S reads do not consume data. +- Completion claims the owner-bound attempt, reads the result, then creates/replaces the display session. Missing/unprepared completion fails closed. S2S reads do not consume data. - SMS/TS43 provider callback is best-effort. A failed token exchange still creates an authenticated **demo display** session containing error data. A successful token followed by a failed session write still attempts error-display adoption, as before. - TS43 rate limits run before target lookup. Status/header differences between SMS/TS43 auth and token endpoints remain in the adapters. - Integrity feature gating and 16 KB parsing precede application execution. Verification uses the trusted incoming hostname. Attempts are claimed before verification; rejected attempts and consumed/failed transactions retain their existing single-use semantics and snapshot credentials/redirects. -A typed boundary is not a new security policy. Existing state validation, PNV authorization, raw diagnostic fields in legacy API responses and provider-response validation are separate hardening work. Provider payloads remain private in the friendly browser error flow but are retained where an existing machine API already returns them. +A typed boundary is not a new security policy. State binding/replay/expiry and token-response validation were explicitly approved as follow-up work; PNV authorization and raw diagnostic fields in legacy machine APIs remain outside that scope. Provider payloads remain private in the friendly browser error flow but are retained where an existing machine API already returns them. ## Verification @@ -41,6 +72,66 @@ The underlying Google verifier and Redis attempt implementation still contain le - Source hashes for `src/server`, `src/shared` and `runtime` matched between working source and verification copy. - Independent review of commits `01d3a8f..d2f0640`: no Critical/Important findings. The reviewer did not independently assess runtime suites; the results above were observed by the implementer. -Deferred minor: if a provider returns HTTP 200 with a `null` token body, the local `access_token` read occurs inside the userinfo provider wrapper. The callback classification becomes `PROVIDER_UNAVAILABLE`/`userinfo` rather than the previous `AUTH_FAILED`/`application`, despite no userinfo request occurring. Valid provider responses and endpoint statuses are unaffected. This diagnostic edge case needs a separate regression/fix before claiming exhaustive malformed-provider equivalence. +Historical deferred minor (resolved by the approved 2026-10-31 hardening): if a provider returns HTTP 200 with a `null` token body, the local `access_token` read occurs inside the userinfo provider wrapper. The callback classification becomes `PROVIDER_UNAVAILABLE`/`userinfo` rather than the previous `AUTH_FAILED`/`application`, despite no userinfo request occurring. Valid provider responses and endpoint statuses are unaffected. This diagnostic edge case needs a separate regression/fix before claiming exhaustive malformed-provider equivalence. Decisions: the approved follow-up uses one combined design/plan; permissive wire inputs remain unknown rather than receiving new rejection rules; verification uses an isolated source copy; the result page has only a type assertion for the previously unvalidated state field, with no rendering change. Costs: less document separation, remaining malformed-input/rendering risks, and no claim that copied-tree verification substitutes for a clean CI/staging run. External provider/device/staging acceptance remains outstanding. + +## Acceptance record (2026-10-31 readability follow-up) + +- The extended SMS/SIM HTTP characterization passed against the original backend, including literal callback payloads, scope, nonce, DCQL and callback → token → userinfo order. Three extra Integrity exception-boundary scenarios also passed against the original service. +- Current source: Node 24.22.0 production build, lint and typecheck passed; 42 unit/HTTP tests, 8 Redis/process integration tests and all 28 Chromium scenarios passed. The optional Docker image smoke test was skipped because no test image was supplied. +- The verification copy's server TypeScript file set and hashes match the working source, including removal of the two unused wrappers. No private config or real provider was used. +- Independent source review against the captured pre-change backend found no material behavioral differences. No frontend UI, Redis atomic implementation, endpoint contract or production authentication policy was changed by this follow-up. + +## Acceptance record (2026-10-31 remaining APIs) + +- Baseline characterization: 10 Auth service/web/mobile tests passed before refactoring, including two added service scenarios for authenticated cancellation and malformed callback-state ordering. HTTP checks also cover repeated/missing S2S reads, mobile target errors and provider rejection. +- Current source: Node 24.22.0 build, lint and typecheck passed; all 44 unit/HTTP tests, 8 Redis/process integrations and 28 Chromium scenarios passed. The optional Docker smoke test remained skipped because no test image was supplied. +- A fresh reviewer compared Auth, S2S, config and GeoIP against captured pre-phase sources and found no material differences. The complete `src/` file set and hashes match the isolated verification copy. +- API paths, headers, statuses, cookies, response precedence and legacy acceptance behavior remain unchanged. This phase did not change frontend UI, session/result stores, runtime or private configuration. No real-provider or deployment verification was performed. + +## Approved authentication hardening (2026-10-31) + +The [customer flow guide](NEXTJS_AUTH_FLOWS.md) describes the changed web protocol. `auth/attempts.ts` owns atomic Redis phase/binding/expiry checks, separate from existing result/session stores. `auth/state-cookies.ts` issues random per-attempt HttpOnly owner and callback cookies. Separate owner cookies avoid simultaneous first attempts rotating a shared binding. Direct attempts are valid for five minutes. QR has two minutes to start; a successful first start atomically opens a separate five-minute completion window. Records remain for ten minutes to explain recently expired requests. QR owner cookies last seven minutes to cover the maximum scan plus completion lifetime; callback cookies and other owner cookies keep five minutes. Redis failure does not create process-local authorization state. + +Callback persistence precedes publishing a completed phase and notification. Failure records contain safe diagnostic metadata; the QR owner can receive the same friendly error without creating an authenticated session. Completion has an atomic claim before session writes. IM overlap uses an owner-authenticated pending page and reuses the stored exchange. Mobile code submission and signed Integrity state stay separate protocols. S2S bearer-state reads are unchanged, and PKCE/ID-token validation and real-provider/device acceptance remain outside this work. + +TS43 token payloads preserve server selection. Public callbacks include a server id and an additive per-server map. Shared authorization-code and CIBA token validation rejects missing/empty/non-string access tokens before userinfo. Item 1's PNV/session policy stays unchanged, including demo error sessions. + +Verification for this follow-up: Node 24.22.0 production build, lint and typecheck passed; 51 unit/HTTP tests, 8 Redis/process integration tests, and all 31 Chromium scenarios passed. A strengthened TS43 test also passed using the secondary server for both auth/token requests. The optional Docker test was skipped (no supplied test image). Current `src`, `runtime`, `tests` and `e2e` file sets/hashes match the isolated verification copy; its only build configuration adaptation remains the Turbopack root. No private configs, active dev server, real provider, commit/push or deployment were used. + +Fresh review found two races, now fixed with failing-then-passing regressions: an IM browser return overlapping backchannel exchange now waits without another exchange, and QR refresh retires the previous status requests before awaiting replacement preparation. Chromium verifies completion through HTTP polling without socket traffic and recovery after status requests fail. Real IM sender/provider/device validation remains an external acceptance boundary. + +QR expiry UX follow-up: the popup now carries the prepare response's `expiresAt`, displays an mm:ss countdown, recomputes it on focus/visibility return, and hides the expired QR with a create-new-code action. The backend five-minute deadline and API contracts are unchanged. Regression was observed failing before implementation; build, lint, typecheck and all 32 Chromium scenarios pass. Fake-clock refresh tests explicitly align fixture deadlines with their simulated browser clock. Working/copy `src` and `e2e` file sets/hashes match. + +QR two-minute scan follow-up: QR prepare now expires after 120 seconds; the first successful start atomically sets a fresh 300-second completion deadline. Replays cannot extend it. Direct flow deadlines remain unchanged. Owner binding lasts 420 seconds for QR; callback bindings remain 300 seconds. Status responses add `started` and the current `expiresAt` so the desktop can hide a scanned code and continue waiting beyond its original scan deadline. Backend expiry confirmation stops recovery; local countdown expiry alone does not discard a valid scan that happened immediately before it. No PNV/session acceptance changes. + +Verification: the deadline/cookie regression failed against the old five-minute QR preparation, then passed. Sequential Node 24.22.0 build, lint/typecheck, 52 unit/HTTP tests, 8 integration tests and 33 Chromium scenarios pass; optional Docker test remains skipped. Browser checks cover 02:00 scan countdown, late scan transitioning to completion, expiry and refresh. No commit/push, real-provider/device or deployment verification. + + +## Provider deadlines and shared status tracking (2026-10-31) + +Provider HTTP calls use the shared form/JSON boundary with Axios and abort deadlines, defaulting to 15000 ms per request (`PROVIDER_HTTP_TIMEOUT_MS`, integer 1..120000). No retry was introduced. Typed timeout errors map to safe `PROVIDER_TIMEOUT` diagnostics; CIBA callback failures still allow its existing token/userinfo sequence. Fixed SMS/SIM scopes and synchronous debug calls remain unchanged. + +The QR polling hook and IM pending page use `features/auth/auth-status.ts` for nonoverlapping polls every 2000 ms, request cancellation, recovery, hard expiry and completion URL validation. The status service can recognize an already finished exact-state authenticated session after owner-cookie removal, without changing session acceptance. See [flow guide](NEXTJS_AUTH_FLOWS.md#provider-deadlines-and-status-recovery). + + +Verification: Node 24.22.0 lint, typecheck and final production build passed; 65 unit/HTTP tests, 8 Redis/process integrations and 43 Chromium scenarios passed. Optional Docker smoke test skipped without a supplied image. New browser regressions failed before implementation and pass now for offline IM recovery and reopening an already finished pending page. Source sets/hashes match the verification copy. A fresh source reviewer found no material issues; the active mobile dev server remains ready. No real-provider/device test, commit/push or deployment was performed. + + +## Automatic public origin (2026-10-31) + +The request identity resolver always determines the origin per request in every environment. A single trusted Kubernetes ingress uses `TRUST_PROXY_HOPS=1` with sanitized forwarded host/protocol; direct access retains `0`. The application adds no domain allowlist. The environment example and Compose default no longer force localhost. Authentication state remains bound to the origin captured at preparation; callbacks still require provider registration. + + +Verification for the initial automatic-origin follow-up: production config with no origin passed. A production HTTP regression covers two forwarded HTTPS domains plus direct HTTP, QR/provider callback URLs, Secure cookies and rejection when an existing attempt changes origin. Build, lint/typecheck, 66 unit/API tests, 8 integration tests and 43 Chromium tests passed; the browser fixture uses automatic origin. Optional Docker smoke remained skipped without an image. Live local QR preparation succeeds for both localhost and LAN IP after restart. No Kubernetes deployment or real-provider acceptance was performed. + + +## Request-derived origin only and dev URLs (2026-10-31) + +The origin override was removed from the app, configuration validation, Compose/env samples and test fixtures. Login rendering and HTTP handlers use the same request identity rules. Cross-replica tests model the shared public host supplied by ingress instead of a fixed application setting. Per-attempt origin binding is unchanged. + +Development startup uses Node network interfaces to print localhost and deduplicated noninternal IPv4 login URLs, with the configured port and a same-network phone QR explanation. No LAN address produces a connect/restart hint. Production retains the existing concise listening message. No external network utility or dependency is required. + + +Verification for complete override removal: lint, typecheck, production build, 66 unit/API tests, 8 integrations and 43 Chromium tests pass. Cross-replica tests now forward a shared public host; the actual dev startup regression verifies local/network URLs and the QR instruction. Source sets/hashes match the isolated copy. A fresh reviewer found no material findings. The active restarted dev server prints its LAN URLs and prepares correct QR URLs from both localhost and LAN requests. Optional Docker smoke skipped without a supplied image; no real-provider or deployment claim. diff --git a/docs/NEXTJS_MIGRATION.md b/docs/NEXTJS_MIGRATION.md index 01962aa..6008f69 100644 --- a/docs/NEXTJS_MIGRATION.md +++ b/docs/NEXTJS_MIGRATION.md @@ -1,7 +1,11 @@ # Next.js migration +> Historical reference: the application now lives at the repository root. Use the [current integration guide](nextjs/README.md) for setup and flows. + The new application is developed independently in `showcase-next/`. Express remains the compatibility baseline. No live traffic switches automatically. +For a customer-oriented walkthrough of the running frontend and its authentication requests, read [Authentication flows](NEXTJS_AUTH_FLOWS.md). + ## Contract inventory @@ -27,22 +31,22 @@ There are 21 business HTTP method/path pairs, including three optional Play Inte | POST `/api/play-integrity/verify` | `api/play-integrity/verify/route.ts` | | POST `/api/play-integrity/token-exchange` | `api/play-integrity/token-exchange/route.ts` | -Also preserve `/socket.io/` transport behavior, required image/icon URLs used by existing config, and useful 404 behavior. Do not reproduce obsolete JS/CSS library URLs in the new app. Add `/health/live` and `/health/ready` for the new service only. Do not create `page.tsx` and `route.ts` for the same route segment. Use explicit 302 responses for legacy redirect endpoints; Next.js redirect defaults are not interchangeable with Express 302. +Preserve required image/icon URLs used by existing config and useful 404 behavior. The new application replaces `/socket.io/` with owner-authenticated `/auth/status` HTTP polling every two seconds. Do not reproduce obsolete JS/CSS library URLs in the new app. Add `/health/live` and `/health/ready` for the new service only. Do not create `page.tsx` and `route.ts` for the same route segment. Use explicit 302 responses for legacy redirect endpoints; Next.js redirect defaults are not interchangeable with Express 302. ## Compatibility ledger -Characterize these before migration; do not silently correct them: +Legacy differences and explicitly approved corrections: 1. QR error page currently checks only string `'false'`, not boolean `false` or missing verification. Web session authentication is not gated on PNV. Preserve demo behavior; integrating applications must apply the stricter rule documented in `docs/PNV.md`. 2. SMS/TS43 token failure still creates an authenticated display session. Keep this behavior visibly documented as demo error display, not a new authorization guarantee. -3. TS43 frontend omits `server_id` in `/ts43/token` despite sending it to `/ts43/auth`; characterize the mismatch. Baseline migration preserves it until an explicit behavior-fix decision is approved. -4. `/api/config` publishes callback URLs without server ID, while actual web auth uses both path parameters. Preserve public response shape/value unless a separate correction is approved; new UI builds authorization via `/auth/start`, not this field. -5. Missing callback state generates a UUID; start only checks presence; cached results are not consumed. OAuth state hardening is separate scope. +3. Approved correction (2026-10-31): TS43 `/ts43/token` now sends the same `server_id` as `/ts43/auth`. +4. Approved correction: `/api/config` publishes the default server's complete `redirect_uri` and adds `redirect_uris` keyed by configured server id. +5. Approved correction: web callers first POST `/auth/prepare`. Server-generated state, per-attempt owner/callback cookies and atomic expiring Redis claims (QR: two minutes to scan, then five minutes to complete; direct: five minutes) replace missing/arbitrary-state acceptance. Callback/completion are single-use; QR owners can recover via `/auth/status`, and overlapping IM browser returns wait on `/auth/pending`. Raw S2S cache reads remain non-consuming. This changes the web protocol intentionally; retain preparation cookies and follow its returned URL. 6. `env` currently depends on `NODE_ENV`. Moving the same deployment label to `APP_ENV` is a required compatibility adaptation, and must be tested in stage. -No provider call retries or broad timeout changes are introduced. Preserve best-effort CIBA callback behavior. Legacy request-body acceptance includes JSON and urlencoded for existing non-Integrity endpoints; duplicate/malformed query/body cases need characterization. Play Integrity keeps its dedicated 16 KB bound and safe 400 responses even when Content-Length is absent. Keep security headers compatible with consent iframe, browser credential APIs and required external assets. Log request ID, method/path without query secrets, status and duration; do not copy credential/token/PII debug dumps into the new service. Logging redaction and safe React rendering are explicit migration adaptations. +Malformed HTTP 200 token responses now fail at the token step before userinfo (`TOKEN_RESPONSE_INVALID`). No provider call retries or broad timeout changes are introduced. Preserve best-effort CIBA callback behavior. Legacy request-body acceptance includes JSON and urlencoded for existing non-Integrity endpoints; duplicate/malformed query/body cases need characterization. Play Integrity keeps its dedicated 16 KB bound and safe 400 responses even when Content-Length is absent. Keep security headers compatible with consent iframe, browser credential APIs and required external assets. Log request ID, method/path without query secrets, status and duration; do not copy credential/token/PII debug dumps into the new service. Logging redaction and safe React rendering are explicit migration adaptations. ## Validation @@ -51,7 +55,7 @@ Run the legacy suite with Node 24.22.0 using `npm test`. Local provider stubs ar ## New application -`showcase-next/` owns React pages, HTTP Route Handlers and a custom Node entrypoint. It does not import the parent Express application. The UI uses shadcn/ui and Tailwind; Socket.IO retains the QR room/event protocol. Redis stores opaque sessions, one-hour auth results, rate counters and atomic Integrity attempts/transactions. +`showcase-next/` owns React pages, HTTP Route Handlers and a custom Node entrypoint. It does not import the parent Express application. The UI uses shadcn/ui and Tailwind; QR and IM read authentication status through HTTP polling every two seconds; Socket.IO is removed. Redis stores opaque sessions, one-hour auth results, rate counters and atomic Integrity attempts/transactions. ### Local setup @@ -67,9 +71,11 @@ npm run config:check npm run dev ``` +Development startup prints `Local` and `Network` login URLs for the actual port. For phone QR authentication, open a Network URL on the computer and put the phone on the same reachable network. A QR containing localhost points to the phone itself. If several interfaces exist, all noninternal IPv4 addresses are listed; choose the one reachable from the phone. No LAN address produces a connect-and-restart hint. + Alternatively, explicitly set `NODE_CONFIG_DIR` to the absolute existing private `config/` directory. The new app does not implicitly search the parent folder or load the parent's `.env`. The existing JSON schema, root/per-flow credential fallbacks, locale and app_config overrides remain supported. Configuration is server-only; public DTOs omit credentials. Arrays in node-config overlays replace the complete inherited array. -`NODE_ENV` is development/test/production. Use `APP_ENV=stage` for demo behavior and provider `env`, and `NODE_CONFIG_ENV=stage` for node-config stage overlays. One canonical browser origin per deployment is proposed: set `APP_ORIGIN` to the exact public scheme/host/port registered with the provider. Play Integrity verification uses the trusted incoming hostname separately. +`NODE_ENV` is development/test/production. Use `APP_ENV=stage` for demo behavior and provider `env`, and `NODE_CONFIG_ENV=stage` for node-config stage overlays. Origin is always detected per request in development and production. For direct access, the app uses the request host/protocol. Behind a single trusted Kubernetes ingress, set `TRUST_PROXY_HOPS=1` so `X-Forwarded-Host` and `X-Forwarded-Proto` supply the public domain and HTTPS protocol. No app domain allowlist is required; ingress controls which domains route here. Register each public callback URL used with the provider. Play Integrity verification uses the trusted incoming hostname separately. The new session cookie is `showcase_next_sid`: a random opaque ID, HttpOnly, SameSite=Lax, host-only, one-hour absolute expiry and Secure on HTTPS. Redis data never goes into this cookie. Existing Express sessions are not imported. @@ -87,13 +93,12 @@ From the repository root: ```bash docker compose -f docker-compose.next.yml build -SHOWCASE_APP_ORIGIN=https://showcase.example.com \ - docker compose -f docker-compose.next.yml up -d +docker compose -f docker-compose.next.yml up -d ``` -The opt-in Compose file leaves the existing stack unchanged. It mounts root `config/default.json` read-only at `/run/showcase-config/default.json` and starts its own Redis volume. Redis is published only on loopback port 6380. Set `SHOWCASE_APP_ENV` and `SHOWCASE_TRUST_PROXY_HOPS` explicitly for deployment. An HTTPS reverse proxy must sanitize incoming forwarded headers, set correct host/protocol/client-IP headers, and support WebSocket upgrade. Prevent clients from bypassing a trusted proxy. Multiple app replicas retaining Socket.IO polling need sticky affinity **and** the Redis adapter. Pub/sub notifications are not durable. +The opt-in Compose file leaves the existing stack unchanged. It mounts root `config/default.json` read-only at `/run/showcase-config/default.json` and starts its own Redis volume. Redis is published only on loopback port 6380. Set `SHOWCASE_APP_ENV` and `SHOWCASE_TRUST_PROXY_HOPS` explicitly for deployment. An HTTPS reverse proxy must sanitize incoming forwarded headers, set correct host/protocol/client-IP headers. Prevent clients from bypassing a trusted proxy. App replicas share attempts/results through Redis, so authentication polling needs no sticky affinity, WebSocket upgrade or pub/sub adapter. -`GET /health/live` checks the HTTP process; `/health/ready` checks runtime config, enabled-feature settings, Redis and socket readiness. Redis failures do not fall back to memory. The container runs as the node user, which must have read permission for mounted configuration/credential files. +`GET /health/live` checks the HTTP process; `/health/ready` checks runtime config, enabled-feature settings, Redis availability. Redis failures do not fall back to memory. The container runs as the node user, which must have read permission for mounted configuration/credential files. To change private JSON, pre-validate a candidate mount with `npm run config:check`, replace the file, and recreate all app containers: @@ -121,7 +126,15 @@ npm run test:e2e Redis integration tests launch their own disposable local `redis-server`; never point them at a deployed store. Browser tests use fake local providers and a temporary configuration directory; they do not read private configuration. TS43 browser credentials and WebOTP are stubbed where needed; this is not real-device evidence. -### Local acceptance evidence (2026-10-28) +### Polling-only local verification (2026-10-31) + +The current application checks `/auth/status` immediately and every 2000 ms for QR and IM. Socket.IO client/server, notifier, Redis adapter/emitter and their dependencies have been removed. Configuration and Redis determine readiness; no authentication WebSocket or sticky affinity is required. + +Node 24.22.0: ESLint, TypeScript and production build passed; 66 unit/API tests, 6 process/Redis integration tests and 43 Chromium scenarios passed in an isolated local fixture. The optional Docker smoke test was skipped. Coverage includes HTTP-only two-browser QR completion, status failure recovery, no socket traffic, expiry/refresh/close, callback on another replica, owner-cookie rejection, sessions after restart and Redis outage. A fresh source review found no material issues. Real providers, mobile devices and the deployed ingress remain external acceptance boundaries. + +### Historical local acceptance evidence (2026-10-28) + +The socket transport checks below describe the earlier implementation. The current polling-only application removes that transport and its dependencies; see [authentication status polling](NEXTJS_AUTH_FLOWS.md#provider-deadlines-and-status-recovery). The implementation is on branch `migration/nextjs`, based on `0db9ad1`, in `/private/tmp/ipification-showcase-nextjs`. Checked with Node 24.22.0: @@ -138,14 +151,14 @@ Regression fixes from the final review pass: The independent reviewer stopped before issuing a complete report because its usage limit was reached. Its confirmed proxy finding was reproduced and fixed with RED/GREEN tests; the author checked malformed-input behavior and added the process-boundary tests above. A complete independent branch review remains outstanding. Do not interpret local test success as staging or production acceptance. -Still pending: actual provider IP/IP+/IM and SMS flows, Android TS43/WebOTP support, Google Play Integrity verification, exact staging proxy topology and public callbacks, multi-replica load-balancer affinity/reconnect behavior, and a rehearsed cutover/rollback. The implementation does not switch traffic or remove Express. +Still pending: actual provider IP/IP+/IM and SMS flows, Android TS43/WebOTP support, Google Play Integrity verification, exact staging proxy topology and public callbacks, multi-replica HTTP polling through the actual ingress, and a rehearsed cutover/rollback. The implementation does not switch traffic or remove Express. ### Cutover and rollback 1. Verify all current flows against the exact staging public origin, provider client/callback registrations and supported devices. Include real IP/IP+/IM, QR on two devices, S2S/mobile APIs, SMS, TS43 and both Integrity verify/bypass hosts. 2. Stop new legacy starts, drain outstanding callbacks/QR sessions, and wait at least the 120-second Integrity TTL. Do not infer drained external flows from cache expiry alone. -3. Switch all auth paths and `/socket.io/` to the new application together. Users must log in again; keep the prior image/config available. -4. Roll back the complete route/socket owner to Express if needed. New sessions are not valid in Express. Do not delete legacy source or Redis data as part of rollback. +3. Switch all auth paths, including `/auth/prepare`, `/auth/status` and `/auth/complete`, to the new application together. Users must log in again; keep the prior image/config available. +4. Roll back all authentication routes to Express if needed. New sessions are not valid in Express. Do not delete legacy source or Redis data as part of rollback. No production traffic switch, push or legacy removal is performed by this implementation. @@ -161,4 +174,16 @@ Verification: production build, lint/typecheck, 14 unit/HTTP tests (including ex ### Showcase tab presentation -PNV, Login and Identity use a scoped segmented navigation style with decorative icons, 46px minimum target height, an elevated white selected tab and primary-color emphasis. Focus remains visible, reduced-motion preferences disable transitions, and decorative icons are hidden below 360px to preserve label space. Tab filtering, accessible labels, hash navigation and stored selection are unchanged. Existing navigation/storage tests and desktop/mobile visual checks passed (3 Chromium scenarios), along with production build, lint and typecheck. +PNV, Login and Identity use a scoped segmented navigation style with decorative icons, 48px minimum target height, a primary-color selected tab with white text and icons, and a neutral navigation background. Focus remains visible, reduced-motion preferences disable transitions, and decorative icons are hidden below 360px to preserve label space. Tab filtering, accessible labels, hash navigation and stored selection are unchanged. Existing navigation/storage tests and desktop/mobile visual checks passed (3 Chromium scenarios), along with production build, lint and typecheck. + +### Mobile interaction in the development preview + +The custom dev server binds to `0.0.0.0`. Next's dev-resource origin guard therefore needs an explicit allowlist for browsers opening the preview through an IP. Without it, the HMR handshake is blocked and the page can stay unhydrated: neither tab selection nor content responds. `next.config.ts` allows loopback and exact IPv4 addresses from the host's network interfaces. Arbitrary external origins remain blocked. Restart `npm run dev` after config or network-interface changes. This setting applies to Next development resources. + +A touch regression switches Login, Identity and PNV and checks selection, URL hash and visible section headings. To run it against an already-running preview (without starting a fixture server), use `E2E_REUSE_SERVER=1 npm run test:e2e -- e2e/login.spec.ts -g 'touch users'`. The dev HTTP contract also verifies successful HMR upgrades for local IP origins and rejection of an unrelated origin. The reported failure was reproduced through IP access, then the touch test passed after the allowlist and restart; Android Chrome emulation also passed through the machine's LAN address. These are Chromium emulation checks, not physical-device evidence. + +### Authentication button presentation + +Flow buttons use equal outlined rows with a method icon, consistent spacing and a directional chevron. Enabled choices have white backgrounds, dark labels and primary-color icon badges; hover/pressed states add a light primary tint. Disabled choices have a neutral surface and legible labels. Solid primary styling is retained for dialog confirmation actions. Shared buttons use 44px default height, with 58px minimum flow rows and reduced-motion support. Authentication handlers, labels and enable/disable conditions are unchanged. Production build, lint/typecheck and all 15 Chromium scenarios passed after the styling change. + +The header reuses the legacy IP logo at `/images/logo.ico` by default. A configured `app_logo_url` (with `app_logo_style`) takes precedence and is displayed in the header rather than duplicated in the hero section. diff --git a/docs/NEXTJS_UI_PARITY.md b/docs/NEXTJS_UI_PARITY.md new file mode 100644 index 0000000..5c43e73 --- /dev/null +++ b/docs/NEXTJS_UI_PARITY.md @@ -0,0 +1,59 @@ +# Next.js legacy UI parity + +The Next.js showcase follows the existing [staging login UI](https://showcase.stage.ipification.com/auth/login). The reference was checked in Chrome on 29 October 2026 and compared with `public/css/style.styl` and the legacy views. + +## Presentation + +- White page with a centered 580px desktop column, the horizontal black IPification logo, Inter typography, underline tabs, and full-width method rows. +- Phone verification uses the inline country/dial-code selector. Stage still supports Wonderland (+999), including switching to and from real countries when configuration permits it. +- Identity places Anonymous Identity before the phone input, matching the legacy content order. +- QR, About, SMS, callback completion, and result screens use the legacy presentation. Callback errors retain the actionable explanation and safe diagnostics introduced during migration. +- Responsive layouts retain keyboard-accessible tabs, labeled controls, dialog focus handling, and reduced-motion support. No Bootstrap or jQuery was introduced. + +The presentation is scoped in `src/features/login/legacy-showcase.css`; React/Radix components and the existing server services remain in use. Available methods, labels, logo overrides, country defaults, and the Site selector still come from configuration, so environments with different configuration can display different content. + +The fallback logo is a local copy of the reference's [official horizontal logo](https://www.ipification.com/resources/logo/horizontalB.svg). Inter font files are served locally; their license is included in `public/fonts/INTER-LICENSE.txt`. + +## Verification + +Production build, ESLint, and TypeScript checks passed. All 18 Chromium scenarios passed against a fake provider and disposable Redis, covering desktop/mobile layout, tabs, country changes, QR callbacks across browser sessions, cancellation, OTP, TS43, and safe callback errors. These checks do not represent a production deployment or a new end-to-end verification against the real identity provider. + +The existing local Chrome page at `http://localhost:3002/auth/login#pnv` was also visually checked at desktop width. This UI update does not change files under `src/server` or `runtime`. + +## Usability additions + +The approved follow-up retains the legacy layout and underline tabs while adding: + +- Phone guidance connected to the input through `aria-describedby`, invalid feedback after leaving the field, and recovery after correcting the number. +- PNV, Login, and Identity omit the additional "About verification methods" section to follow the original screens. +- Pending indicators and an immediate activation guard. Cancelling SIM verification unlocks a new attempt; completion of an older attempt cannot unlock the newer one. +- HTTP status polling every two seconds for QR and IM, with no Socket.IO connection. Visible QR connection/waiting states and a button to create a new QR after connection failure or its server-issued deadline. QR has two minutes to start, then a separate five-minute completion window after a valid scan; provider expiry may differ. Refresh generates a fresh state, remounts the dialog, and cancels the previous status requests. Same-origin and exact-state validation still apply to completion URLs. +- Result presentation now follows the legacy title, full userinfo JSON and client id/state layout. JSON is visible immediately and preserves true/false values, their types, and all additional claims. When `phone_number_verified` is present, a short note explains phone verification outcomes and the string `"false"` truthiness pitfall. If the field is absent, no explanation is rendered. Copy JSON and its actionable clipboard-denial message remain available. No endpoint, provider response or session acceptance policy is changed. + +Five additional Chromium scenarios cover invalid-input recovery, QR waiting timeout and stale callbacks, connection recovery, result disclosure/copy, and SIM cancellation/retry isolation. All 23 browser scenarios passed using the fake provider and disposable Redis. + +Tab focus presentation tracks keyboard navigation separately from page/control focus. Pointer selection stays free of surrounding outlines across blur/focus restoration; Tab and arrow-key navigation retain a visible keyboard indicator. Browser shortcuts with Ctrl, Alt, or Meta do not switch the page into keyboard-navigation mode. The original bottom underline remains visible. + +## Result visibility follow-up + +The full original userinfo is visible by default for boolean/string true and false. Selected-field highlights and the technical-details disclosure were removed to restore the legacy reading order. Nested claims, false, zero, null and string/boolean types are preserved in both JSON and clipboard output. A compact note explains successful/unsuccessful/missing verification and safe backend comparison. Authentication/session policy and the separate phone QR success/error screens are unchanged. + +Build, lint/typecheck and all 37 Chromium scenarios pass. Four new true/false/type scenarios failed with hidden JSON before the fix, then passed. Desktop and 375px mobile screenshots were inspected; no horizontal overflow was found. Working source/test/e2e file sets and hashes match the isolated verification copy. The active mobile development server on port 3002 remains ready. + +## Phone entry compatibility follow-up + +The upgraded phone library defaults to strict input-length rejection, unlike the legacy entry behavior. International input now explicitly disables strict length rejection and formatting while typing; the legacy digits-only sanitization remains. Nonempty input enables method buttons, and the existing validity check runs when a method is activated, before prepare/SMS/SIM calls. Clearing input disables methods, including Wonderland, and normalization still uses the library for real-country numbers. No provider/session policy changes were made. + +Two browser regressions first reproduced early method disabling and digit truncation, then passed. Build, lint and typecheck pass; the complete 39-scenario Chromium suite passed, followed by 4/4 phone scenarios including the user's exact Serbia (+381) eighth-to-ninth digit case (40 distinct scenarios verified). Live dev verification also typed 123456789 sequentially with +381 after a full reload. The local dev server remains available for mobile testing; no real-provider authentication was performed. + +Result-help visibility: the explanation is rendered only when the original userinfo has its own `phone_number_verified` field; the check does not use truthiness, so false remains included. Missing-claim regression failed before the change, then all five result browser scenarios (missing, boolean true/false, string true/false) passed. Build, lint/typecheck and diff checks passed. Raw userinfo, auth and session behavior are unchanged. + +## QR dialog layout follow-up (2026-10-31) + +The QR dialog uses a centered 420px maximum-width layout with 16px viewport clearance and a viewport-bounded scroll area. Header, QR quiet area and status/countdown remain in separate flow rows. Short landscape screens use two columns so the full QR and its instructions fit together; unusually small viewports can scroll. Recovery states keep the same layout. Mobile uses 24px padding; the close target is 44px. Content, colors, QR deadlines, polling and direct mobile authentication are unchanged. + +A hydration warning observed in the user's Chrome showed an extra `cz-shortcut-listen="true"` body attribute. The app does not render that attribute; the same URL in a clean Chrome context had no console errors. Extension-induced markup differences are not suppressed in app code. + +## Initial tab selection + +On the initial server render, a loading screen is shown until browser preferences are read, then the header and tab content appear together. The URL hash takes precedence over the saved tab; unavailable or blocked storage falls back to the first configured tab. This avoids flashing the PNV tab before the saved selection appears. diff --git a/docs/PLAY_INTEGRITY.md b/docs/PLAY_INTEGRITY.md index 2ec7a10..1bb8ca3 100644 --- a/docs/PLAY_INTEGRITY.md +++ b/docs/PLAY_INTEGRITY.md @@ -1,5 +1,7 @@ # Play Integrity backend demo +> Historical reference: the application now lives at the repository root. Use the [current integration guide](nextjs/README.md) for setup and flows. + > A separate Next.js application is being migrated in `showcase-next/`. See the [migration guide](NEXTJS_MIGRATION.md) for its runtime configuration, commands, compatibility limits and verification. The existing Express application remains available. diff --git a/docs/PLAY_INTEGRITY_IPIFICATION_DEMO.md b/docs/PLAY_INTEGRITY_IPIFICATION_DEMO.md index b4ba798..8e27bdf 100644 --- a/docs/PLAY_INTEGRITY_IPIFICATION_DEMO.md +++ b/docs/PLAY_INTEGRITY_IPIFICATION_DEMO.md @@ -1,5 +1,7 @@ # Google Play Integrity + IPification — mobile demo +> Historical reference: the application now lives at the repository root. Use the [current integration guide](nextjs/README.md) for setup and flows. + This document describes an Android demo that checks Google Play Integrity before starting an IPification authentication. ## Scope diff --git a/docs/nextjs/README.md b/docs/nextjs/README.md new file mode 100644 index 0000000..7dfd8b2 --- /dev/null +++ b/docs/nextjs/README.md @@ -0,0 +1,31 @@ +# Next.js integration guide + +This guide is the starting point for the Next.js showcase documentation. It explains the current behavior of the application and where to find its implementation. It is for developers integrating IPification as well as developers running the demo. The examples use placeholders: obtain your own credentials, enabled products, callback URLs and supported scopes from your IPification configuration. + +## Choose a flow + +| Goal | Showcase methods | Guide | +| --- | --- | --- | +| Verify an entered number | IP, IP+, IP via App, IM | [Browser authorization](browser-flows.md) | +| Log in with a mobile identity | IP, IP+, IM | [Browser authorization](browser-flows.md) | +| Obtain anonymous identity or verify KYC phone | Anonymous Identity, KYC | [Browser authorization](browser-flows.md) | +| Verify or obtain a number using a SIM credential | PNV SIM, Login SIM | [SIM / TS43](sim.md) | +| Verify an entered number with an OTP | PNV SMS | [SMS](sms.md) | +| Start on desktop and continue on a phone | QR for supported browser flows | [QR and polling](qr.md) | + +[Run the demo](running.md) · [Showcase HTTP endpoints](api.md) · [Source walkthrough](../NEXTJS_AUTH_FLOWS.md) + +## Read the source + +- Start with [the screen](../../src/features/login/showcase.tsx), then [method selection and preparation](../../src/features/login/use-verification.ts). +- For browser redirects, follow [web handlers](../../src/server/auth/web.ts), [auth service](../../src/server/auth/service.ts), then [provider calls](../../src/server/auth/provider.ts). [Attempt storage](../../src/server/auth/attempts.ts) handles browser bindings, expiry and concurrent callbacks. +- For QR, follow [status polling](../../src/features/auth/auth-status.ts). For SMS and SIM, start with [SMS UI](../../src/features/sms/use-sms-flow.tsx) or [SIM UI](../../src/features/ts43/use-ts43-flow.tsx), then [CIBA provider calls](../../src/server/ciba/provider.ts). +- [Play Integrity service](../../src/server/integrity/service.ts) is a separate optional Android flow. [Session store](../../src/server/session/store.ts) holds demo display sessions; it is not the provider's user session. + +## Who calls whom? + +The **browser** calls the showcase's own endpoints. The **showcase backend** holds client credentials and calls the IPification authorization, token, userinfo and CIBA endpoints. In your integration, replace the showcase backend with your own backend and register your own callback URI. Paths such as `/auth/prepare`, `/auth/status`, `/sms/auth` and `/ts43/token` are showcase implementation choices, not IPification API requirements. + +Keep client secrets and access tokens on the backend. Examples are educational and do not constitute a complete production OAuth implementation: validate state and redirect URI, use HTTPS, and implement provider-supported OAuth/OIDC protections, including PKCE and ID-token validation where applicable. The showcase does not itself implement PKCE or ID-token validation in the browser flow. Never log credentials, authorization codes, tokens, OTPs or full userinfo. + +Older top-level guides describe earlier implementations; use this guide and the current source for Next.js behavior. diff --git a/docs/nextjs/api.md b/docs/nextjs/api.md new file mode 100644 index 0000000..6eb434d --- /dev/null +++ b/docs/nextjs/api.md @@ -0,0 +1,42 @@ +# Showcase Next.js HTTP reference + +[Guide home](README.md) · [Running](running.md) + +These paths belong to **the showcase**, not the IPification provider. Unless noted, requests use the showcase origin. Preserve cookies for browser flows. JSON responses may contain provider-dependent fields; do not assume example fields are always present. See the linked route/service code for exact behavior and error handling. + +## Browser authentication + +| Method and path | Purpose | +| --- | --- | +| `GET /` | Redirect to login | +| `GET /auth/login` | Login, PNV and Identity screen | +| `POST /auth/prepare` | Create an owner-bound attempt; JSON `user_flow`, `server_id`, boolean `qr`, optional `phone`. Returns HTTP 201 with `state`, `url`, `expiresAt` and an HttpOnly owner cookie. | +| `GET /auth/start` | Begin a previously prepared attempt and redirect to authorization; use the returned `url` rather than constructing one. | +| `GET /auth/callback/:userFlow/:serverId` | Provider redirect; consumes callback parameters including `state`, `code`, `error` and `error_description`. | +| `GET /auth/status?state=...` | Owner-bound status polling: `pending`, `completed`, `failed` or `finished`; may include `started`, `expiresAt`, `completionUrl`. | +| `GET /auth/complete?state=...` | Claim a completed attempt and establish its display session. | +| `GET /auth/pending` | Internal waiting page used in applicable IM backchannel flows. | +| `GET /auth/qrcode/complete`, `/auth/qrcode/error` | Phone result pages for QR. | +| `GET /auth/error`, `/auth/login/error`, `/auth/session-required` | Error/session handling pages or redirects. | +| `GET /user/info` | Session-backed result page. | + +The login UI uses `/auth/prepare` before `/auth/start`; calling the old start URL without a valid prepared attempt is not equivalent. The QR and IM waiting logic polls status every two seconds; these routes and cookies are demo-specific. [Browser flow](browser-flows.md) · [QR](qr.md) · [Auth handlers](../../src/server/auth/web.ts) + +## JSON and operational endpoints + +| Method and path | Purpose | +| --- | --- | +| `GET /api/config` | Public configuration; credentials are omitted. | +| `GET /api/geoip` | Approximate country and resolved client IP; default country used on lookup miss. | +| `POST /auth/mobile/login` | Exchange supplied `client_id`, `server_id`, `code` and `redirect_uri`; returns `auth_server` plus userinfo. Does not create a browser session. | +| `POST /auth/s2s/signin` | Read previously cached raw userinfo by `state`; cache miss is an empty 401. This is not a new code exchange. | +| `POST /sms/auth`, `/sms/token` | Start SMS and exchange OTP. See [SMS](sms.md). | +| `POST /ts43/auth`, `/ts43/token` | Start SIM credential request and exchange credential. See [SIM](sim.md). `/ts43/auth` can return 429. | +| `POST /sms/log`, `/ts43/log` | Diagnostic endpoints; not provider integration steps. Avoid sending sensitive information. | +| `GET /health/live` | Process liveness. | +| `GET /health/ready` | Checks runtime configuration and Redis; returns 503 when unavailable. | +| `POST /api/play-integrity/attempt`, `/verify`, `/token-exchange` | Optional Play Integrity flow; full paths share `/api/play-integrity`. Disabled endpoints return 404 unless `PLAY_INTEGRITY_ENABLED=true`. See [Play Integrity](../PLAY_INTEGRITY_IPIFICATION_DEMO.md); verify its deployment assumptions against Next.js code. | + +In the SMS and TS43 rows, each second path has the same prefix shown in its first path. `/auth/mobile/login` and `/auth/s2s/signin` preserve demo API contracts; their existence does not establish an authorization policy suitable for arbitrary internet-facing clients. Review access controls and protect sensitive requests in your deployment. + +For CIBA responses, a successful start includes `auth_server`, `auth_req_id`, `nonce`, and for SIM `digital_request`. Token success returns `auth_server` and provider userinfo. Failures can return JSON or, for some auth errors, text. Session creation can occur for error data; it does not prove verification. See [handlers](../../src/server/ciba/handlers.ts). diff --git a/docs/nextjs/browser-flows.md b/docs/nextjs/browser-flows.md new file mode 100644 index 0000000..a8d1ccb --- /dev/null +++ b/docs/nextjs/browser-flows.md @@ -0,0 +1,81 @@ +# Browser authorization: PNV, Login and Identity + +[Guide home](README.md) · [QR](qr.md) · [Endpoint reference](api.md) + +## Methods and inputs + +| Screen | Configured `user_flow` values | Showcase behavior | +| --- | --- | --- | +| PNV | `pvn_ip`, `pvn_ip_plus`, `pvn_im`, `pvn_ipificator` | Requires the entered phone; passes it as `login_hint` | +| Login | `login_ip`, `login_ip_plus`, `login_im` | Does not ask for a phone | +| Identity | `anonymous`, `kyc_phone` | Anonymous omits the phone; KYC requires it | + +SIM and SMS use separate [SIM](sim.md) and [SMS](sms.md) paths. Availability, client ID, scope, channel and server are configuration-dependent; IP and IP+ use the same redirect machinery with their own configured client and scope. IP via App uses the special IPificator authorization endpoint and is offered on mobile by the showcase. IM navigates directly, including on desktop; other supported desktop browser methods use QR. See [flow catalog](../../src/shared/flow-catalog.ts) and [frontend entry points](../../src/features/login/use-verification.ts). + +## PNV via IP: end to end + +```mermaid +sequenceDiagram + participant B as Browser + participant S as Showcase backend + participant I as IPification + B->>S: POST /auth/prepare (pvn_ip, phone, qr=false) + S-->>B: state, start URL, owner cookie + B->>S: GET start URL + S-->>B: redirect to authorization URL + B->>I: authorize with state and login_hint + I-->>B: redirect to callback with state and code or error + B->>S: GET /auth/callback/pvn_ip/{serverId} + S->>I: POST token, then POST userinfo + S-->>B: redirect through /auth/complete + B->>S: GET /user/info using display session +``` + +1. Configure an auth server and a `pvn_ip` client with the provider-issued credentials and scope. Register the exact callback for the origin and server being used: `{origin}/auth/callback/pvn_ip/{serverId}`. +2. The browser submits a normalized phone in `phone` to the showcase. The demo creates an attempt and an HttpOnly owner cookie. Both the cookie and `state` must remain available to the initiating browser. +3. `/auth/start` builds the provider authorization request with `response_type=code`, `client_id`, `redirect_uri`, configured `scope`, `state`, a nonce, and, when present, `login_hint` and configured `channel`. +4. On callback the backend validates its attempt binding. A provider error is handled as an error; without an error, a code is required. It exchanges the code server-side with the same redirect URI, then retrieves userinfo. Never exchange the code with a client secret in browser code. +5. The showcase stores the result and creates a display session. `/user/info` shows returned userinfo; fields depend on the provider and granted scope. + +Minimal browser example **for this showcase**, executed on its own origin: + +```js +const response = await fetch('/auth/prepare', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'same-origin', + body: JSON.stringify({ + user_flow: 'pvn_ip', server_id: 'stage', + phone: '+', qr: false, + }), +}); +if (!response.ok) throw new Error('Could not prepare authentication'); +const { url } = await response.json(); +window.location.assign(url); +``` + +For your own backend, the provider exchange is conceptually a form POST (do not expose its response or credentials to the browser): + +```js +const form = new URLSearchParams({ + grant_type: 'authorization_code', code, + client_id: clientId, client_secret: clientSecret, redirect_uri: callbackUrl, +}); +const tokenResponse = await fetch(`${oidcRoot}/token`, { method: 'POST', body: form }); +if (!tokenResponse.ok) throw new Error('Token exchange failed'); +const { access_token } = await tokenResponse.json(); +if (!access_token) throw new Error('Missing access token'); +const userResponse = await fetch(`${oidcRoot}/userinfo`, { + method: 'POST', body: new URLSearchParams({ access_token }), +}); +if (!userResponse.ok) throw new Error('Userinfo request failed'); +const userInfo = await userResponse.json(); +``` + +Here `oidcRoot` is `{server.url}/realms/{realm}/protocol/openid-connect`. This illustrates the provider calls in the showcase; it omits the required secure callback/state/session management. See [provider](../../src/server/auth/provider.ts) and [HTTP client](../../src/server/shared/http-client.ts). + +## Read the result correctly + +Preserve the provider's userinfo response. `phone_number_verified` can be absent; absence is not a positive verification. If present, use the provider's documented type and meaning; the demo displays explanatory text whenever the field is present, including guidance for `true`, `false`, and other values. A demo display session alone is not proof of phone verification. The caller must apply its own verification policy to the provider result. + +On provider `error` or `error_description`, the demo shows an error; if none and no code is supplied, it shows a generic failure. A code may be single-use: start a new attempt after a failed/expired flow instead of replaying a callback. Provider or network timeouts can also fail the exchange. Never put raw codes, tokens or sensitive userinfo into logs or shareable URLs. diff --git a/docs/nextjs/qr.md b/docs/nextjs/qr.md new file mode 100644 index 0000000..c77ea02 --- /dev/null +++ b/docs/nextjs/qr.md @@ -0,0 +1,36 @@ +# Desktop QR and polling + +[Guide home](README.md) · [Browser flows](browser-flows.md) + +QR connects two separate browser contexts. The desktop prepares the attempt and owns its cookie; the phone scans the QR and visits the showcase's start URL. The phone completes authorization and callback, while the desktop checks the result. The QR does not contain the client secret. + +```mermaid +sequenceDiagram + participant D as Desktop browser + participant S as Showcase backend and Redis + participant P as Phone browser + participant I as IPification + D->>S: POST /auth/prepare (qr=true) + S-->>D: state, start URL, owner cookie, expiry + D->>S: GET /auth/status?state=... (every 2s) + D-->>P: QR with start URL + P->>S: GET start URL + S-->>P: redirect to provider + P->>I: authenticate + I-->>P: callback with code or error + P->>S: callback; backend stores outcome + D->>S: GET /auth/status?state=... + S-->>D: terminal status and completion URL + D->>S: GET /auth/complete?state=... + S-->>D: display session and result redirect +``` + +The backend allows **two minutes to start scanning**. Once the phone starts the attempt, a separate five-minute completion window starts. Provider timeouts may differ. A fresh QR requires a fresh attempt. These values are showcase behavior, not a provider-wide expiry guarantee. + +The initiating browser polls immediately and then every two seconds. `GET /auth/status?state=...` checks the state, the per-attempt owner cookie, expiry and request origin. It does not bind by client IP. After completion, an authenticated display session with the exact state may report `finished`. A different browser knowing only the state cannot normally read the status. The response is private and not cached. Keep the cookie on the initiating browser and use the same origin throughout; do not copy it to the phone. + +For local testing, open the desktop app with a LAN IP or hostname reachable from the phone **before generating the QR**. `localhost` in a QR refers to the phone itself. The provider must also accept the exact callback URL. See [running locally](running.md). QR/polling are showcase implementation choices: a customer can use a different secure completion mechanism. + +See [polling implementation](../../src/features/auth/auth-status.ts), [QR dialog](../../src/features/qr/qr-dialog.tsx), [attempt storage](../../src/server/auth/attempts.ts), and [status handler](../../src/server/auth/web.ts). + +The phone result page currently treats the string `"false"` as failure but boolean `false` as success, while the desktop result preserves and displays the original userinfo. This legacy-compatible behavior is covered by a contract test; do not use the phone result page as the verification decision. Apply your own provider-claim policy. diff --git a/docs/nextjs/running.md b/docs/nextjs/running.md new file mode 100644 index 0000000..277b3f5 --- /dev/null +++ b/docs/nextjs/running.md @@ -0,0 +1,45 @@ +# Run the Next.js showcase + +[Guide home](README.md) · [QR](qr.md) + +## Local setup + +Use Node.js 24.22.0, npm, Redis and provider-issued credentials. From the repository root: + +```bash +npm ci +cp config/default.json.sample config/default.json +cp .env.example .env.local +# Configure your auth server and clients in config/default.json. +# Set REDIS_URL in .env.local to your reachable Redis instance. +npm run config:check +APP_ENV=stage npm run dev +``` + +Do not commit real credentials. Alternatively set `NODE_CONFIG_DIR` to an absolute private configuration directory. `APP_ENV=stage` selects stage demo behavior and is sent as the provider environment; it does not by itself select node-config's stage overlay. Set `NODE_CONFIG_ENV=stage` separately if that overlay is intended. The sample environment uses port 3002 and Redis `127.0.0.1:6380`; ensure Redis is actually running there or change `REDIS_URL`. + +Startup lists local and network URLs. For phone QR testing, open the desktop through the network URL that the phone can reach, and use that same origin for the flow. Do not use `localhost` in the QR: on the phone it refers to the phone. Register the exact callback URL with your provider. Restart the dev server after changing host network interfaces so its development-origin configuration is refreshed. + +## Checks and common failures + +- Visit `/health/live` for liveness and `/health/ready` for config and Redis readiness. A ready response does not prove the provider credentials or the full authentication flow work. +- If no QR appears, check browser console/network requests, `/auth/prepare`, `/auth/status`, Redis and the reachable origin. The current implementation uses polling, not Socket.IO. +- If callback or token exchange fails, check server/client selection, exact registered redirect URI, scope and provider configuration. Do not replay an authorization code; create a fresh attempt. +- If a phone cannot connect, check the selected LAN address, network routing and host firewall. LAN reachability is separate from successful authentication. + +For production, use HTTPS and configure `TRUST_PROXY_HOPS` only for the actual trusted proxy chain. The ingress must overwrite untrusted forwarded headers and direct access must not bypass it. The root Compose file builds the application, mounts its private config read-only and runs a dedicated Redis container. + +## Playwright checks for the main flows + +The E2E fixture starts a local mock provider and a disposable Redis process. Install `redis-server` and Playwright's Chromium browser, then run a production build before the browser tests: + +```bash +npm run build +E2E_PORT=3103 npm run test:e2e -- e2e/main-flows.spec.ts e2e/qr.spec.ts e2e/sms.spec.ts e2e/result.spec.ts e2e/auth-error.spec.ts +``` + +The focused suite covers mobile-width PNV IP/IP+, Login IP/IP+, Anonymous Identity, KYC, a provider error, QR, SMS and userinfo display. The fixture does not call the real provider or exercise a physical device, carrier or real OTP. IM and SIM are outside this focused acceptance set; existing separate tests remain available. Use a free `E2E_PORT`; do not set `E2E_REUSE_SERVER` when using the fixture. + +## GitHub Actions + +The `Showcase Next.js CI` workflow runs on pushes and pull requests. It installs Node 24.22.0, Redis and Chromium, then runs lint, typecheck, a production build, unit/API tests and the focused Playwright suite above. Failure diagnostics from Playwright are uploaded as a short-lived artifact. The browser fixture starts its own Redis and mock provider; the workflow requires no stage credentials and does not verify live provider or device behavior. The separate process/Redis integration suite is not part of this workflow. diff --git a/docs/nextjs/sim.md b/docs/nextjs/sim.md new file mode 100644 index 0000000..1ed5dc4 --- /dev/null +++ b/docs/nextjs/sim.md @@ -0,0 +1,35 @@ +# SIM / TS43 + +[Guide home](README.md) · [Showcase endpoints](api.md) + +The showcase supports `pvn_sim` for an entered phone and `login_sim` for obtaining a number. The flow requires an eligible carrier, device and browser supporting the digital credential request; selecting SIM does not guarantee support. Configure the appropriate client and provider credentials. + +1. Browser calls `POST /ts43/auth` with `client_id`, `server_id`, `login_hint`, `carrier_hint`, `scope`, and optionally `operation`. In the current demo PNV uses the phone and `openid ip:phone_verify`; Login uses `anonymous`, `openid ip:phone` and `GetPhoneNumber`. The demo currently sends `carrier_hint: 51010`; do not assume that value is suitable for your deployment. +2. The showcase calls provider `/ext/ciba/auth`, then `/ext/bc/ts43/dcql`, returning `auth_req_id`, `nonce` and `digital_request`. +3. A supported browser receives that request through `navigator.credentials.get`. The demo extracts `vp_token` from `credential.token` or `credential.data` under `vp_token['ipification.com'][0]`. +4. Browser sends the credential and request identifiers to `POST /ts43/token`. The backend submits it to provider `/ext/bc/ts43/callback`, calls `/token` with the CIBA grant, then calls `/userinfo`. + +```js +// `started` is the successful JSON response from showcase POST /ts43/auth. +if (!navigator.credentials?.get) throw new Error('SIM is unavailable'); +const credential = await navigator.credentials.get({ + digital: { requests: [started.digital_request] }, +}); +const data = credential?.token || credential?.data; +const vpToken = data?.vp_token?.['ipification.com']?.[0]; +if (!vpToken) throw new Error('No supported SIM credential'); +const response = await fetch('/ts43/token', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + credentials: 'same-origin', + body: JSON.stringify({ + vp_token: vpToken, auth_req_id: started.auth_req_id, nonce: started.nonce, + client_id: '', server_id: 'stage', + }), +}); +if (!response.ok) throw new Error('SIM verification failed'); +const userInfo = await response.json(); +``` + +`/ts43/auth` is rate limited and may return 429 with `Retry-After`. Handle unavailable/cancelled credentials, provider errors and expired attempts. The demo submits the callback best-effort before token exchange; its display session can hold error data, so do not equate session existence with verification success. The demo currently navigates to its result page even if `/ts43/token` returned an error; integrations should check the response explicitly, as above. `/ts43/log` is diagnostic only and should not receive sensitive data. + +See [SIM browser flow](../../src/features/ts43/use-ts43-flow.tsx), [CIBA handler](../../src/server/ciba/handlers.ts), and [provider requests](../../src/server/ciba/provider.ts). diff --git a/docs/nextjs/sms.md b/docs/nextjs/sms.md new file mode 100644 index 0000000..d0f62aa --- /dev/null +++ b/docs/nextjs/sms.md @@ -0,0 +1,35 @@ +# PNV via SMS + +[Guide home](README.md) · [Showcase endpoints](api.md) + +SMS uses a CIBA request to send a one-time code. Configure the provider server and a `pvn_sms` client with credentials and an authorized scope. The showcase asks for a normalized phone number; no client secret is sent to the browser. + +1. Browser calls showcase `POST /sms/auth` with `client_id`, `server_id`, `login_hint` and `scope`. +2. Showcase calls the provider's `/ext/ciba/auth` with `channel=sms`; on success it returns `auth_req_id` and `nonce` to the browser. +3. User enters the OTP. Browser calls `POST /sms/token` with `code`, `auth_req_id`, `nonce`, `client_id`, and `server_id`. +4. Showcase submits the OTP to provider `/ext/bc/sms/callback`, exchanges `auth_req_id` at `/token` with the CIBA grant, and calls `/userinfo`. A successful response includes provider userinfo; `/user/info` uses the display session. + +```js +async function post(path, body) { + const response = await fetch(path, { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + credentials: 'same-origin', body: JSON.stringify(body), + }); + if (!response.ok) throw new Error('SMS request failed'); + return response.json(); +} +const client_id = '', server_id = 'stage'; +const attempt = await post('/sms/auth', { + client_id, server_id, login_hint: '+', + scope: 'openid ip:phone_verify', +}); +// Collect the code from the user; keep it out of logs. +const result = await post('/sms/token', { + client_id, server_id, auth_req_id: attempt.auth_req_id, + nonce: attempt.nonce, code: '', +}); +``` + +These are showcase endpoints, not provider endpoints. The demo submits the provider callback on a best-effort basis and still attempts token exchange after callback failure; that is current behavior, not a general integration recommendation. Error responses are not always JSON (for example a missing client may return text), so production clients should handle both. The showcase can create a display session even on token failure; check the HTTP response and provider result, not session existence. If the code fails or expires, show a useful error and start a new attempt when necessary. + +`/sms/log` is a debug endpoint used by the demo, not a required provider step. Avoid sending it sensitive data in integrations. See [browser SMS flow](../../src/features/sms/use-sms-flow.tsx), [CIBA service](../../src/server/ciba/service.ts), and [provider requests](../../src/server/ciba/provider.ts). diff --git a/showcase-next/e2e/auth-error.spec.ts b/e2e/auth-error.spec.ts similarity index 56% rename from showcase-next/e2e/auth-error.spec.ts rename to e2e/auth-error.spec.ts index c6bbea8..2d3a636 100644 --- a/showcase-next/e2e/auth-error.spec.ts +++ b/e2e/auth-error.spec.ts @@ -2,8 +2,14 @@ import { test, expect } from "@playwright/test"; test("callback failure explains client configuration and lets the user restart login", async ({ page, }) => { + const prepared = await page.request.post("/auth/prepare", { + data: { user_flow: "login_ip", server_id: "stage", qr: false }, + }); + const attempt = await prepared.json(); + // Capture the callback binding without following the fixture provider's success redirect. + await page.request.get(attempt.url, { maxRedirects: 0 }); await page.goto( - "/auth/callback/login_ip/stage?state=fixture-qrcode&code=browser-invalid-client", + `/auth/callback/login_ip/stage?state=${attempt.state}&code=browser-invalid-client`, ); await expect(page).toHaveURL(/\/auth\/error\?/); await expect( @@ -41,3 +47,19 @@ test("callback failure explains client configuration and lets the user restart l await page.getByRole("link", { name: "Try again", exact: true }).click(); await expect(page).toHaveURL(/\/auth\/login$/); }); + +test("callback error opens a dismissible popup on mobile", async ({ page }) => { + await page.setViewportSize({ width: 375, height: 812 }); + await page.route("**/auth/login/error", (route) => + route.fulfill({ json: { error_message: "msisdn_country_mismatch" } }), + ); + await page.goto("/auth/login#pnv"); + const dialog = page.getByRole("dialog", { name: "msisdn_country_mismatch" }); + await expect(dialog).toBeVisible(); + await expect(page.locator("[role=tab]").filter({ hasText: "PNV" })).toBeVisible(); + await expect.poll(async () => Math.round((await dialog.boundingBox())!.y)).toBe(100); + const bounds = await dialog.boundingBox(); + expect(bounds!.width).toBeLessThan(375); + await page.keyboard.press("Escape"); + await expect(dialog).not.toBeVisible(); +}); diff --git a/e2e/auth-status.spec.ts b/e2e/auth-status.spec.ts new file mode 100644 index 0000000..c886ba7 --- /dev/null +++ b/e2e/auth-status.spec.ts @@ -0,0 +1,42 @@ +import { test, expect } from "@playwright/test"; +test("reopening the pending page shows an already completed result without exchanging again", async ({ + page, +}) => { + const response = await page.request.post("/auth/prepare", { + data: { user_flow: "login_im", server_id: "stage", qr: false }, + }); + const attempt = await response.json(); + await page.request.get(attempt.url, { maxRedirects: 0 }); + const callback = await page.request.get( + `/auth/callback/login_im/stage?state=${attempt.state}&code=fixture-code`, + { maxRedirects: 0 }, + ); + const completed = await page.request.get(callback.headers().location, { + maxRedirects: 0, + }); + expect(completed.status()).toBe(302); + await page.goto(`/auth/pending?state=${attempt.state}`); + await expect(page).toHaveURL(/\/user\/info$/); +}); +test("pending IM reports lost connection and recovers when status requests resume", async ({ + page, +}) => { + const prepared = await page.request.post("/auth/prepare", { + data: { user_flow: "login_im", server_id: "stage", qr: false }, + }); + const attempt = await prepared.json(); + await page.request.get(attempt.url, { maxRedirects: 0 }); + let offline = true; + await page.route("**/auth/status?**", (route) => + offline ? route.abort() : route.continue(), + ); + await page.goto(`/auth/pending?state=${attempt.state}`); + await expect(page.getByText(/Connection interrupted/)).toBeVisible(); + await page.request.get( + `/auth/callback/login_im/stage?state=${attempt.state}&code=fixture`, + { headers: { "ip-backchannel-im-auth": "true" }, maxRedirects: 0 }, + ); + offline = false; + await page.getByRole("button", { name: "Retry now", exact: true }).click(); + await expect(page).toHaveURL(/\/user\/info$/); +}); diff --git a/showcase-next/e2e/bootstrap.spec.ts b/e2e/bootstrap.spec.ts similarity index 100% rename from showcase-next/e2e/bootstrap.spec.ts rename to e2e/bootstrap.spec.ts diff --git a/e2e/flow-routing.spec.ts b/e2e/flow-routing.spec.ts new file mode 100644 index 0000000..20daafc --- /dev/null +++ b/e2e/flow-routing.spec.ts @@ -0,0 +1,86 @@ +import { test, expect } from "@playwright/test"; + +test("mobile IP redirects with the normalized phone and a non-QR state", async ({ + page, +}) => { + await page.setViewportSize({ width: 375, height: 812 }); + await page.route("**/auth/start?**", (route) => + route.fulfill({ body: "Intercepted verification navigation" }), + ); + await page.goto("/auth/login#pnv"); + // The phone control loads asynchronously; enter digits after it is attached. + await expect( + page.getByRole("button", { name: /Change country for phone number/ }), + ).toBeVisible(); + await page.getByLabel("Phone number", { exact: true }).fill("2025550123"); + await expect( + page.getByRole("button", { name: "IP", exact: true }), + ).toBeEnabled(); + const request = page.waitForRequest( + (request) => new URL(request.url()).pathname === "/auth/start", + ); + await page.getByRole("button", { name: "IP", exact: true }).click(); + const query = new URL((await request).url()).searchParams; + expect(query.get("user_flow")).toBe("pvn_ip"); + expect(query.get("server_id")).toBe("stage"); + expect(query.get("phone")).toBe("12025550123"); + expect(query.get("state")).toMatch(/^[0-9a-f-]{36}$/); +}); + +test("desktop messaging login redirects directly without attaching a phone number", async ({ + page, +}) => { + await page.route("**/auth/start?**", (route) => + route.fulfill({ body: "Intercepted verification navigation" }), + ); + await page.goto("/auth/login#login"); + const request = page.waitForRequest( + (request) => new URL(request.url()).pathname === "/auth/start", + ); + await page.getByRole("button", { name: "IM", exact: true }).click(); + const query = new URL((await request).url()).searchParams; + expect(query.get("user_flow")).toBe("login_im"); + expect(query.has("phone")).toBe(false); + expect(query.get("state")).toMatch(/^[0-9a-f-]{36}$/); +}); + +test("desktop identity QR distinguishes anonymous identity and KYC phone input", async ({ + page, +}) => { + await page.goto("/auth/login#identity"); + await page + .getByRole("button", { name: "Anonymous Identity", exact: true }) + .click(); + let query = new URL( + (await page.getByTestId("qr-auth-link").getAttribute("href"))!, + ).searchParams; + expect(query.get("user_flow")).toBe("anonymous"); + expect(query.has("phone")).toBe(false); + expect(query.get("state")).toMatch(/-qrcode$/); + await page.getByRole("button", { name: "Close", exact: true }).click(); + await page.getByLabel("Phone number", { exact: true }).fill("2025550123"); + await page.getByRole("button", { name: "KYC", exact: true }).click(); + query = new URL( + (await page.getByTestId("qr-auth-link").getAttribute("href"))!, + ).searchParams; + expect(query.get("user_flow")).toBe("kyc_phone"); + expect(query.get("phone")).toBe("12025550123"); + expect(query.get("state")).toMatch(/-qrcode$/); +}); + +test("IM pending page adopts the owner's stored backchannel result", async ({ + page, +}) => { + const prepared = await page.request.post("/auth/prepare", { + data: { user_flow: "login_im", server_id: "stage", qr: false }, + }); + const attempt = await prepared.json(); + await page.request.get(attempt.url, { maxRedirects: 0 }); + const backchannel = await page.request.get( + `/auth/callback/login_im/stage?state=${attempt.state}&code=fixture`, + { headers: { "ip-backchannel-im-auth": "true" }, maxRedirects: 0 }, + ); + expect(backchannel.status()).toBe(200); + await page.goto(`/auth/pending?state=${attempt.state}`); + await expect(page).toHaveURL(/\/user\/info$/); +}); diff --git a/e2e/legacy-layout.spec.ts b/e2e/legacy-layout.spec.ts new file mode 100644 index 0000000..5446a94 --- /dev/null +++ b/e2e/legacy-layout.spec.ts @@ -0,0 +1,103 @@ +import { test, expect } from "@playwright/test"; +import { startApp, freePort } from "../tests/helpers/processes"; +import { configFixture } from "../tests/helpers/config"; +import { startRedis } from "../tests/helpers/redis"; +test("legacy single-column layout and Identity content order remain usable", async ({ + page, +}) => { + await page.setViewportSize({ width: 1440, height: 1000 }); + await page.goto("/auth/login#pnv"); + const main = page.locator(".legacy-main"); + await expect(main).toBeVisible(); + expect((await main.boundingBox())!.width).toBe(580); + await expect(page.getByText("Mobile identity", { exact: true })).toHaveCount( + 0, + ); + const logo = page.locator(".legacy-brand-logo"); + await expect(logo).toHaveAttribute( + "src", + "/images/logo/ipification-horizontal-black.svg", + ); + expect( + await logo.evaluate( + (el: HTMLImageElement) => el.complete && el.naturalWidth > 0, + ), + ).toBe(true); + await page.getByRole("tab", { name: "Identity", exact: true }).click(); + const anonymous = page.getByRole("button", { + name: "Anonymous Identity", + exact: true, + }); + const phone = page.getByLabel("Phone number", { exact: true }); + await expect(anonymous).toBeVisible(); + await expect(phone).toBeVisible(); + expect((await anonymous.boundingBox())!.y).toBeLessThan( + (await phone.boundingBox())!.y, + ); + await expect(phone).toHaveCount(1); + await page.keyboard.press("Tab"); + await phone.focus(); + await expect(phone).toHaveCSS("outline-style", "solid"); + await expect(phone).toHaveCSS("outline-width", "2px"); +}); +test("legacy mobile tabs and phone controls fit without horizontal scrolling", async ({ + page, +}) => { + await page.setViewportSize({ width: 375, height: 812 }); + await page.goto("/auth/login#pnv"); + for (const name of ["Login", "Identity", "PNV"]) { + const tab = page.getByRole("tab", { name, exact: true }); + await tab.click(); + await expect(tab).toHaveAttribute("aria-selected", "true"); + expect( + await page.evaluate(() => document.documentElement.scrollWidth), + ).toBeLessThanOrEqual(375); + } +}); +test("inline stage country picker still switches between Wonderland and a real country", async ({ + page, +}) => { + const redis = await startRedis(), + config = configFixture({ disabled_select_country: false }); + const port = await freePort(); + const app = await startApp({ + NODE_ENV: "production", + APP_ENV: "stage", + NODE_CONFIG_ENV: "stage", + NODE_CONFIG_DIR: config.dir, + PORT: String(port), + REDIS_URL: redis.url, + }); + try { + await page.goto(app.url + "/auth/login#pnv"); + await page.getByLabel("Phone number", { exact: true }).fill("123456789"); + await expect( + page.getByRole("button", { name: "IP", exact: true }), + ).toBeEnabled(); + await page.getByLabel("Country", { exact: true }).selectOption("us"); + await page.getByLabel("Phone number", { exact: true }).fill("2025550123"); + await expect( + page.getByRole("button", { name: "IP", exact: true }), + ).toBeEnabled(); + await page.getByLabel("Country", { exact: true }).selectOption("ww"); + await expect(page.getByLabel("Phone number", { exact: true })).toHaveValue( + "", + ); + await expect(page.locator(".legacy-stage-phone")).toContainText("+999"); + await expect( + page.getByRole("button", { name: "IP", exact: true }), + ).toBeDisabled(); + await page.setViewportSize({ width: 375, height: 812 }); + expect( + await page.evaluate(() => document.documentElement.scrollWidth), + ).toBeLessThanOrEqual(375); + await page.screenshot({ + path: "test-results/legacy-stage-mobile.png", + fullPage: true, + }); + } finally { + await app.stop(); + config.remove(); + await redis.stop(); + } +}); diff --git a/e2e/login.spec.ts b/e2e/login.spec.ts new file mode 100644 index 0000000..9b407ed --- /dev/null +++ b/e2e/login.spec.ts @@ -0,0 +1,107 @@ +import { test, expect } from "@playwright/test"; +test("configured flow tabs and preferences survive refresh without Bootstrap", async ({ + page, +}) => { + await page.goto("/auth/login"); + await expect( + page.getByRole("tab", { name: "PNV", exact: true }), + ).toHaveAttribute("aria-selected", "true"); + await page.getByRole("tab", { name: "Login", exact: true }).click(); + await expect(page).toHaveURL(/#login$/); + await page.reload(); + await expect( + page.getByRole("tab", { name: "Login", exact: true }), + ).toHaveAttribute("aria-selected", "true"); + expect( + await page.locator('script[src*="jquery"],link[href*="bootstrap"]').count(), + ).toBe(0); +}); +test("blocked localStorage does not prevent configured flow selection", async ({ + page, +}) => { + await page.addInitScript(() => { + Storage.prototype.getItem = () => { + throw new DOMException("Blocked", "SecurityError"); + }; + Storage.prototype.setItem = () => { + throw new DOMException("Blocked", "SecurityError"); + }; + }); + await page.goto("/auth/login"); + await page.getByRole("tab", { name: "Identity", exact: true }).click(); + await expect( + page.getByRole("button", { name: "Anonymous Identity", exact: true }), + ).toBeVisible(); +}); + +test("touch users can switch every flow tab on mobile Chrome", async ({ + browser, +}) => { + const context = await browser.newContext({ + viewport: { width: 393, height: 851 }, + deviceScaleFactor: 2.75, + isMobile: true, + hasTouch: true, + userAgent: + "Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36", + baseURL: `http://127.0.0.1:${process.env.E2E_PORT || "3002"}`, + }); + const page = await context.newPage(); + try { + await page.goto("/auth/login"); + for (const [name, hash, title] of [ + ["Login", "login", "Quick Access"], + ["Identity", "identity", "Mobile ID & Know Your Customer"], + ["PNV", "pnv", "Phone Number Verification"], + ]) { + const tab = page.getByRole("tab", { name, exact: true }); + await tab.tap(); + await expect(tab).toHaveAttribute("aria-selected", "true"); + await expect(page).toHaveURL(new RegExp(`#${hash}$`)); + await expect( + page.getByRole("heading", { name: title, exact: true }), + ).toBeVisible(); + } + } finally { + await context.close(); + } +}); + +test("server render shows loading until browser preferences are ready", async ({ + browser, +}) => { + const context = await browser.newContext({ + javaScriptEnabled: false, + baseURL: `http://127.0.0.1:${process.env.E2E_PORT || "3002"}`, + }); + try { + const page = await context.newPage(); + await page.goto("/auth/login#login"); + await expect(page.locator("main")).toHaveAttribute("aria-busy", "true"); + await expect(page.getByRole("status")).toHaveText("Loading…"); + await expect(page.locator(".legacy-brand-logo")).toHaveCount(0); + await expect(page.getByRole("tab", { name: "PNV" })).toHaveCount(0); + await expect( + page.getByRole("heading", { name: "Phone Number Verification" }), + ).toHaveCount(0); + } finally { + await context.close(); + } +}); + +test("initial tab prefers hash over saved preference", async ({ page }) => { + await page.addInitScript(() => localStorage.setItem("selector", "identity")); + await page.goto("/auth/login#login"); + await expect( + page.getByRole("tab", { name: "Login", exact: true }), + ).toHaveAttribute("aria-selected", "true"); + await expect(page.locator("main")).toHaveAttribute("aria-busy", "false"); + await page.goto("/auth/login"); + await expect( + page.getByRole("tab", { name: "Identity", exact: true }), + ).toHaveAttribute("aria-selected", "true"); + await page.goto("/auth/login#invalid"); + await expect( + page.getByRole("tab", { name: "Identity", exact: true }), + ).toHaveAttribute("aria-selected", "true"); +}); diff --git a/e2e/main-flows.spec.ts b/e2e/main-flows.spec.ts new file mode 100644 index 0000000..fb288b0 --- /dev/null +++ b/e2e/main-flows.spec.ts @@ -0,0 +1,76 @@ +import { expect, test } from "@playwright/test"; + +// The local fixture provider redirects back with a code and returns fixed userinfo. +// These tests exercise the real Showcase backend and browser session; no real provider is called. +for (const flow of [ + { tab: "pnv", button: "IP", userFlow: "pvn_ip", phone: true }, + { tab: "pnv", button: "IP+", userFlow: "pvn_ip_plus", phone: true }, + { tab: "login", button: "IP", userFlow: "login_ip", phone: false }, + { tab: "login", button: "IP+", userFlow: "login_ip_plus", phone: false }, + { + tab: "identity", + button: "Anonymous Identity", + userFlow: "anonymous", + phone: false, + }, + { tab: "identity", button: "KYC", userFlow: "kyc_phone", phone: true }, +] as const) { + test(`mobile ${flow.userFlow} completes through the provider and displays userinfo`, async ({ + page, + }) => { + await page.setViewportSize({ width: 375, height: 812 }); + await page.goto(`/auth/login#${flow.tab}`); + if (flow.phone) { + await expect( + page.getByRole("button", { name: /Change country for phone number/ }), + ).toBeVisible(); + await page.getByLabel("Phone number", { exact: true }).fill("2025550123"); + } + const authorize = page.waitForRequest((request) => { + const url = new URL(request.url()); + return url.pathname.endsWith("/protocol/openid-connect/auth"); + }); + await page.getByRole("button", { name: flow.button, exact: true }).click(); + const query = new URL((await authorize).url()).searchParams; + expect(query.get("redirect_uri")).toContain( + `/auth/callback/${flow.userFlow}/stage`, + ); + expect(query.get("response_type")).toBe("code"); + expect(query.get("login_hint")).toBe(flow.phone ? "12025550123" : null); + await expect(page).toHaveURL(/\/user\/info$/); + const userInfo = JSON.parse((await page.locator("pre").textContent())!); + expect(userInfo.sub).toBe("browser-user"); + expect(userInfo.phone_number_verified).toBe("true"); + }); +} + +test("a provider error returns to login and shows the callback message", async ({ + page, +}) => { + await page.setViewportSize({ width: 375, height: 812 }); + await page.route("**/auth/start?**", async (route) => { + // Let the app set the callback cookie, then simulate a provider error redirect. + const start = await route.fetch({ maxRedirects: 0 }); + const authorization = new URL(start.headers().location); + const callback = new URL(authorization.searchParams.get("redirect_uri")!); + callback.searchParams.set( + "state", + authorization.searchParams.get("state")!, + ); + callback.searchParams.set("error", "access_denied"); + await route.fulfill({ + response: start, + headers: { ...start.headers(), location: callback.href }, + }); + }); + await page.goto("/auth/login#pnv"); + await expect( + page.getByRole("button", { name: /Change country for phone number/ }), + ).toBeVisible(); + await page.getByLabel("Phone number", { exact: true }).fill("2025550123"); + await page.getByRole("button", { name: "IP", exact: true }).click(); + await expect(page).toHaveURL(/\/auth\/login/); + await expect( + page.getByRole("dialog", { name: "access_denied" }), + ).toBeVisible(); +}); diff --git a/e2e/phone.spec.ts b/e2e/phone.spec.ts new file mode 100644 index 0000000..b608f2c --- /dev/null +++ b/e2e/phone.spec.ts @@ -0,0 +1,100 @@ +import { test, expect } from "@playwright/test"; +test("phone verification validates input and works at mobile width", async ({ + page, +}) => { + await page.setViewportSize({ width: 375, height: 812 }); + await page.goto("/auth/login#pnv"); + const phone = page.getByLabel("Phone number", { exact: true }); + await expect(phone).toBeVisible(); + await phone.fill("2025550123"); + await expect( + page.getByRole("button", { name: "IP", exact: true }), + ).toBeEnabled(); + expect( + await page.evaluate( + () => document.documentElement.scrollWidth <= window.innerWidth, + ), + ).toBe(true); +}); + +test("short digit input remains editable and enables methods; validation runs on activation", async ({ + page, +}) => { + await page.goto("/auth/login#pnv"); + const phone = page.getByLabel("Phone number", { exact: true }); + const ip = page.getByRole("button", { name: "IP", exact: true }); + await expect(ip).toBeDisabled(); + await phone.pressSequentially("1"); + await expect(phone).toHaveValue("1"); + await expect(ip).toBeEnabled(); + await phone.pressSequentially("23456789"); + expect((await phone.inputValue()).replace(/\D/g, "")).toBe("123456789"); + await phone.fill("1"); + let prepares = 0; + await page.route("**/auth/prepare", (route) => { + prepares++; + return route.abort(); + }); + await ip.click(); + await expect( + page.getByRole("alert").filter({ hasText: "Your phone is invalid" }), + ).toBeVisible(); + expect(prepares).toBe(0); + await phone.fill(""); + await expect(ip).toBeDisabled(); +}); + +test("phone input does not truncate a typed digit sequence at the country's valid length", async ({ + page, +}) => { + await page.goto("/auth/login#pnv"); + const phone = page.getByLabel("Phone number", { exact: true }); + await phone.pressSequentially("123456789123456789"); + expect((await phone.inputValue()).replace(/\D/g, "")).toBe( + "123456789123456789", + ); +}); + +test("Serbia +381 accepts the ninth typed digit", async ({ page }) => { + await page.goto("/auth/login#pnv"); + await page + .getByRole("button", { name: /Change country for phone number/ }) + .click(); + await page + .getByRole("combobox", { name: "Search", exact: true }) + .fill("Serbia"); + await page + .getByRole("option", { name: "Serbia (+381)", exact: true }) + .click(); + const phone = page.getByLabel("Phone number", { exact: true }); + await phone.pressSequentially("12345678"); + await expect(phone).toHaveValue("12345678"); + await phone.pressSequentially("9"); + await expect(phone).toHaveValue("123456789"); + await page.screenshot({ + path: "test-results/phone-serbia-nine-digits.png", + fullPage: true, + }); +}); + +test("phone input has no surrounding outline for pointer entry and retains keyboard focus indication", async ({ + page, +}) => { + await page.goto("/auth/login#pnv"); + const phone = page.getByLabel("Phone number", { exact: true }); + await phone.click(); + await phone.pressSequentially("123456789"); + await expect(phone).toHaveCSS("outline-style", "none"); + await expect(phone).toHaveCSS("box-shadow", "none"); + await phone.press("ArrowLeft"); + await expect(phone).toHaveCSS("outline-style", "none"); + await phone.evaluate((input: HTMLInputElement) => { + input.blur(); + input.focus(); + }); + await expect(phone).toHaveCSS("outline-style", "none"); + await phone.press("Tab"); + await phone.focus(); + await expect(phone).toHaveCSS("outline-style", "solid"); + await expect(phone).toHaveCSS("outline-width", "2px"); +}); diff --git a/e2e/qr-layout.spec.ts b/e2e/qr-layout.spec.ts new file mode 100644 index 0000000..8efd8d4 --- /dev/null +++ b/e2e/qr-layout.spec.ts @@ -0,0 +1,54 @@ +import { test, expect } from "@playwright/test"; + +for (const viewport of [ + { width: 320, height: 568 }, + { width: 400, height: 483 }, + { width: 667, height: 320 }, + { width: 1440, height: 1000 }, +]) { + test(`QR content stays separated and scrollable at ${viewport.width}x${viewport.height}`, async ({ + page, + }) => { + const errors: string[] = []; + page.on("pageerror", (error) => errors.push(error.message)); + page.on("console", (message) => { + if (message.type() === "error") errors.push(message.text()); + }); + // Open the desktop QR flow before resizing; mobile direct auth stays unchanged. + await page.setViewportSize({ width: 1440, height: 1000 }); + await page.goto("/auth/login#login"); + await page.getByRole("button", { name: "IP", exact: true }).click(); + await expect(page.getByTestId("qr-auth-link")).toHaveAttribute( + "href", + /qrcode/, + ); + await page.setViewportSize(viewport); + const dialog = page.getByRole("dialog"); + await expect(async () => { + const bounds = await dialog.boundingBox(); + expect(bounds!.x).toBeGreaterThanOrEqual(15); + expect(bounds!.y).toBeGreaterThanOrEqual(15); + expect(bounds!.x + bounds!.width).toBeLessThanOrEqual( + viewport.width - 15, + ); + expect(bounds!.y + bounds!.height).toBeLessThanOrEqual( + viewport.height - 15, + ); + }).toPass({ timeout: 2000 }); + // QR and instructions must never overlap, in stacked or landscape layouts. + const qr = await dialog.locator(".legacy-qr-content svg").boundingBox(); + const caption = await dialog.locator(".legacy-qr-caption").boundingBox(); + expect( + qr!.y + qr!.height + 16 <= caption!.y || + qr!.x + qr!.width + 16 <= caption!.x, + ).toBe(true); + await dialog.getByText(/Expires in/).scrollIntoViewIfNeeded(); + await expect(dialog.getByText(/Expires in/)).toBeInViewport(); + await page.screenshot({ + path: `test-results/qr-layout-${viewport.width}x${viewport.height}.png`, + }); + await dialog.getByRole("button", { name: "Close", exact: true }).click(); + await expect(dialog).not.toBeVisible(); + expect(errors).toEqual([]); + }); +} diff --git a/e2e/qr.spec.ts b/e2e/qr.spec.ts new file mode 100644 index 0000000..22ee745 --- /dev/null +++ b/e2e/qr.spec.ts @@ -0,0 +1,209 @@ +import { test, expect } from "@playwright/test"; +test("desktop QR receives callback from independent phone browser session", async ({ + page, + browser, +}) => { + await page.goto("/auth/login#login"); + await page.getByRole("button", { name: "IP", exact: true }).click(); + await expect(page.getByRole("dialog")).toBeVisible(); + const link = page.getByTestId("qr-auth-link"); + const url = await link.getAttribute("href"); + expect(url).toContain("-qrcode"); + const phone = await browser.newContext(); + try { + const phonePage = await phone.newPage(); + await phonePage.goto(url!); + await expect( + phonePage.getByRole("heading", { name: "Success", exact: true }), + ).toBeVisible(); + await expect(page).toHaveURL(/\/user\/info$/); + await expect(page.locator("pre")).toContainText("browser-user"); + } finally { + await phone.close(); + } +}); +test("closed QR cannot navigate desktop on a late callback", async ({ + page, + browser, +}) => { + await page.goto("/auth/login#login"); + await page.getByRole("button", { name: "IP", exact: true }).click(); + const url = await page.getByTestId("qr-auth-link").getAttribute("href"); + await page.getByRole("button", { name: "Close", exact: true }).click(); + await expect(page.getByRole("dialog")).not.toBeVisible(); + const phone = await browser.newContext(); + try { + const p = await phone.newPage(); + await p.goto(url!); + await expect( + p.getByRole("heading", { name: "Success", exact: true }), + ).toBeVisible(); + await expect(page).toHaveURL(/auth\/login/); + } finally { + await phone.close(); + } +}); +test("QR owner receives completion through HTTP polling without socket traffic", async ({ + page, + browser, +}) => { + const socketRequests: string[] = []; + page.on("request", (request) => { + if (request.url().includes("/socket.io/")) + socketRequests.push(request.url()); + }); + page.on("websocket", (socket) => socketRequests.push(socket.url())); + await page.goto("/auth/login#login"); + await page.getByRole("button", { name: "IP", exact: true }).click(); + const url = await page.getByTestId("qr-auth-link").getAttribute("href"); + const phone = await browser.newContext(); + try { + await (await phone.newPage()).goto(url!); + await expect(page).toHaveURL(/\/user\/info$/, { timeout: 10000 }); + expect(socketRequests).toEqual([]); + } finally { + await phone.close(); + } +}); + +test("refresh retires the old QR while preparing a replacement", async ({ + page, +}) => { + await page.clock.install(); + await page.goto("/auth/login#login"); + await page.getByRole("button", { name: "IP", exact: true }).click(); + await expect(page.getByTestId("qr-auth-link")).toHaveAttribute( + "href", + /qrcode/, + ); + await page.clock.fastForward(120001); + let release!: () => void; + const held = new Promise((resolve) => { + release = resolve; + }); + await page.route("**/auth/prepare", async (route) => { + await held; + const response = await route.fetch(); + const attempt = await response.json(); + const now = await page.evaluate(() => Date.now()); + await route.fulfill({ + response, + json: { ...attempt, expiresAt: now + 120000 }, + }); + }); + await page + .getByRole("button", { name: "Create new QR code", exact: true }) + .click(); + try { + await expect(page.getByRole("dialog")).not.toBeVisible(); + } finally { + release(); + } + await expect(page.getByTestId("qr-auth-link")).toHaveAttribute( + "href", + /qrcode/, + ); +}); + +test("QR countdown uses the backend deadline and hides expired codes", async ({ + page, +}) => { + const now = new Date(); + await page.clock.install({ time: new Date(now.getTime() - 60000) }); + await page.clock.pauseAt(now); + await page.route("**/auth/prepare", async (route) => { + const response = await route.fetch(); + const attempt = await response.json(); + await route.fulfill({ + response, + json: { ...attempt, expiresAt: now.getTime() + 10000 }, + }); + }); + await page.goto("/auth/login#login"); + await page.getByRole("button", { name: "IP", exact: true }).click(); + await expect(page.getByTestId("qr-auth-link")).toHaveAttribute( + "href", + /qrcode/, + ); + await expect( + page.getByText("Expires in 00:10", { exact: true }), + ).toBeVisible(); + await page.clock.fastForward(6000); + await expect( + page.getByText("Expires in 00:04", { exact: true }), + ).toBeVisible(); + await page.clock.fastForward(4000); + await expect( + page.getByText("QR code expired", { exact: true }), + ).toBeVisible(); + await expect(page.getByTestId("qr-auth-link")).toHaveCount(0); + await expect( + page.getByRole("button", { name: "Create new QR code", exact: true }), + ).toBeVisible(); +}); + +test("QR scan window is two minutes and scanning switches to the completion countdown", async ({ + page, +}) => { + const now = new Date(); + await page.clock.install({ time: new Date(now.getTime() - 60000) }); + await page.clock.pauseAt(now); + let scanned = false; + await page.route("**/auth/prepare", async (route) => { + const response = await route.fetch(); + const attempt = await response.json(); + expect(attempt.expiresAt - Date.now()).toBeLessThanOrEqual(120000); + await route.fulfill({ + response, + json: { ...attempt, expiresAt: now.getTime() + 120000 }, + }); + }); + await page.route("**/auth/status?**", async (route) => { + await route.fulfill({ + json: { + status: "pending", + started: scanned, + expiresAt: now.getTime() + (scanned ? 419000 : 120000), + }, + }); + }); + await page.goto("/auth/login#login"); + await page.getByRole("button", { name: "IP", exact: true }).click(); + await expect( + page.getByText("Expires in 02:00", { exact: true }), + ).toBeVisible(); + await page.clock.fastForward(119000); + scanned = true; + await page.clock.fastForward(3000); + await expect(page.getByText(/Verification in progress/)).toBeVisible(); + await page.clock.fastForward(1000); + await expect(page.getByText(/Complete verification in 04:/)).toBeVisible(); + await expect( + page.getByText("QR code expired", { exact: true }), + ).not.toBeVisible(); + await expect(page.getByTestId("qr-auth-link")).toHaveCount(0); +}); + +test("QR close has no initial focus ring and remains accessible with the keyboard", async ({ + page, +}) => { + await page.goto("/auth/login#login"); + const trigger = page.getByRole("button", { name: "IP", exact: true }); + await trigger.click(); + const dialog = page.getByRole("dialog"); + const close = dialog.getByRole("button", { name: "Close", exact: true }); + await expect(dialog).toBeFocused(); + await expect(close).not.toBeFocused(); + await expect(close).toHaveCSS("box-shadow", "none"); + await expect(page.getByTestId("qr-auth-link")).toHaveAttribute( + "href", + /qrcode/, + ); + await page.keyboard.press("Tab"); + await expect(page.getByTestId("qr-auth-link")).toBeFocused(); + await page.keyboard.press("Tab"); + await expect(close).toBeFocused(); + await expect(close).not.toHaveCSS("box-shadow", "none"); + await page.keyboard.press("Enter"); + await expect(dialog).not.toBeVisible(); +}); diff --git a/e2e/result.spec.ts b/e2e/result.spec.ts new file mode 100644 index 0000000..1df4825 --- /dev/null +++ b/e2e/result.spec.ts @@ -0,0 +1,56 @@ +import { test, expect } from "@playwright/test"; +for (const [name, verified] of [ + ["missing", undefined], + ["boolean-true", true], + ["boolean-false", false], + ["string-true", "true"], + ["string-false", "false"], +] as const) { + test(`userinfo is visible immediately with the original ${name} verification value`, async ({ + page, + }) => { + if (verified === false) + await page.setViewportSize({ width: 375, height: 812 }); + const prepared = await page.request.post("/auth/prepare", { + data: { user_flow: "pvn_ip", server_id: "stage", qr: false }, + }); + const attempt = await prepared.json(); + await page.request.get(attempt.url, { maxRedirects: 0 }); + await page.goto( + `/auth/callback/pvn_ip/stage?state=${attempt.state}&code=fixture-result-${name}`, + ); + await expect(page).toHaveURL(/\/user\/info$/); + const json = page.locator("pre"); + await expect(json).toBeVisible(); + expect(JSON.parse((await json.textContent())!)).toEqual({ + sub: "browser-user", + phone_number: "+12025550123", + ...(verified === undefined ? {} : { phone_number_verified: verified }), + custom_claim: { enabled: false, count: 0, value: null }, + }); + const explanation = page.getByRole("complementary", { + name: "Phone verification explanation", + }); + if (verified === undefined) { + await expect(explanation).toHaveCount(0); + expect(JSON.parse((await json.textContent())!)).not.toHaveProperty( + "phone_number_verified", + ); + } else { + await expect(explanation).toBeVisible(); + await expect( + page.getByText("Phone verification succeeded.", { exact: true }), + ).toBeVisible(); + await expect( + page.getByText(/Phone verification did not succeed/), + ).toBeVisible(); + } + expect( + await page.evaluate(() => document.documentElement.scrollWidth), + ).toBeLessThanOrEqual(verified === false ? 375 : 1280); + await page.screenshot({ + path: `test-results/result-${name}.png`, + fullPage: true, + }); + }); +} diff --git a/showcase-next/e2e/sms.spec.ts b/e2e/sms.spec.ts similarity index 100% rename from showcase-next/e2e/sms.spec.ts rename to e2e/sms.spec.ts diff --git a/showcase-next/e2e/stage.spec.ts b/e2e/stage.spec.ts similarity index 82% rename from showcase-next/e2e/stage.spec.ts rename to e2e/stage.spec.ts index 005f6a0..f1c1a01 100644 --- a/showcase-next/e2e/stage.spec.ts +++ b/e2e/stage.spec.ts @@ -13,13 +13,18 @@ test("stage keeps Wonderland 999 country and disabled selector option", async ({ APP_ENV: "stage", NODE_CONFIG_ENV: "stage", NODE_CONFIG_DIR: config.dir, - APP_ORIGIN: `http://127.0.0.1:${port}`, PORT: String(port), REDIS_URL: redis.url, }); try { await page.goto(app.url + "/auth/login#pnv"); const input = page.getByLabel("Phone number", { exact: true }); + const ip = page.getByRole("button", { name: "IP", exact: true }); + await expect(ip).toBeDisabled(); + await input.fill("1"); + await expect(ip).toBeEnabled(); + await input.fill(""); + await expect(ip).toBeDisabled(); await input.fill("123456789"); await expect( page.getByRole("button", { name: "IP", exact: true }), diff --git a/e2e/tab-focus.spec.ts b/e2e/tab-focus.spec.ts new file mode 100644 index 0000000..23b4e38 --- /dev/null +++ b/e2e/tab-focus.spec.ts @@ -0,0 +1,31 @@ +import { test, expect } from "@playwright/test"; + +test("restoring focus after a pointer-selected tab does not restore a surrounding outline", async ({ + page, +}) => { + await page.goto("/auth/login#pnv"); + const tab = page.getByRole("tab", { name: "Login", exact: true }); + await tab.click(); + await expect(tab).toHaveCSS("outline-style", "none"); + // Model the control blur/focus boundary when Chrome restores the selected page. + await tab.evaluate((el: HTMLElement) => { + el.blur(); + el.focus(); + }); + await expect(tab).toHaveCSS("outline-style", "none"); + await expect(tab).toHaveCSS("box-shadow", "none"); + await expect(tab).toHaveCSS("border-bottom-color", "rgb(0, 0, 0)"); +}); + +test("keyboard users retain a visible tab focus indicator after using the mouse", async ({ + page, +}) => { + await page.goto("/auth/login#pnv"); + const tab = page.getByRole("tab", { name: "Login", exact: true }); + await tab.click(); + await page.keyboard.press("ArrowRight"); + const identity = page.getByRole("tab", { name: "Identity", exact: true }); + await expect(identity).toBeFocused(); + await expect(identity).toHaveCSS("outline-style", "solid"); + await expect(identity).toHaveCSS("outline-width", "2px"); +}); diff --git a/showcase-next/e2e/ts43.spec.ts b/e2e/ts43.spec.ts similarity index 68% rename from showcase-next/e2e/ts43.spec.ts rename to e2e/ts43.spec.ts index 8f7330a..58ebb05 100644 --- a/showcase-next/e2e/ts43.spec.ts +++ b/e2e/ts43.spec.ts @@ -11,7 +11,17 @@ test("TS43 sends credential response to token endpoint and renders results", asy }); }); await page.goto("/auth/login#login"); + await page.getByRole("combobox", { name: "Authentication server" }).click(); + await page.getByRole("option", { name: "secondary", exact: true }).click(); + const authRequest = page.waitForRequest( + (request) => new URL(request.url()).pathname === "/ts43/auth", + ); + const tokenRequest = page.waitForRequest( + (request) => new URL(request.url()).pathname === "/ts43/token", + ); await page.getByRole("button", { name: "SIM", exact: true }).click(); + expect((await authRequest).postDataJSON().server_id).toBe("secondary"); + expect((await tokenRequest).postDataJSON().server_id).toBe("secondary"); await expect(page).toHaveURL(/\/user\/info$/); }); test("null credentials show cancellation and keep login available", async ({ diff --git a/e2e/usability.spec.ts b/e2e/usability.spec.ts new file mode 100644 index 0000000..bc5df2f --- /dev/null +++ b/e2e/usability.spec.ts @@ -0,0 +1,173 @@ +import { test, expect } from "@playwright/test"; + +test("phone guidance distinguishes empty and invalid input and clears after correction", async ({ + page, +}) => { + await page.goto("/auth/login#pnv"); + const phone = page.getByLabel("Phone number", { exact: true }); + await expect( + page.getByText("Enter a phone number to enable verification methods.", { + exact: true, + }), + ).toBeVisible(); + await phone.fill("123"); + await phone.blur(); + await expect(phone).toHaveAttribute("aria-invalid", "true"); + await expect( + page.getByText("Enter a valid phone number for the selected country.", { + exact: true, + }), + ).toBeVisible(); + await phone.fill("2025550123"); + await expect(phone).toHaveAttribute("aria-invalid", "false"); + await expect( + page.getByRole("button", { name: "IP", exact: true }), + ).toBeEnabled(); + for (const tab of ["pnv", "login", "identity"]) { + await page.goto(`/auth/login#${tab}`); + await expect(page.locator(".legacy-showcase")).toHaveAttribute( + "data-tab", + tab, + ); + await expect( + page.getByText("About verification methods", { exact: true }), + ).toHaveCount(0); + } +}); + +test("QR waiting timeout permits a new state and ignores the old callback", async ({ + page, + browser, +}) => { + await page.clock.install(); + // Advancing only the browser clock must also advance mocked server deadlines. + await page.route("**/auth/prepare", async (route) => { + const response = await route.fetch(); + const attempt = await response.json(); + const now = await page.evaluate(() => Date.now()); + await route.fulfill({ + response, + json: { ...attempt, expiresAt: now + 120000 }, + }); + }); + await page.goto("/auth/login#login"); + await page.getByRole("button", { name: "IP", exact: true }).click(); + const oldUrl = await page.getByTestId("qr-auth-link").getAttribute("href"); + await expect( + page.getByText("Waiting for verification…", { exact: true }), + ).toBeVisible(); + await page.clock.fastForward(120_001); + await expect( + page.getByText("QR code expired", { exact: true }), + ).toBeVisible(); + await expect(page.getByTestId("qr-auth-link")).toHaveCount(0); + await page + .getByRole("button", { name: "Create new QR code", exact: true }) + .click(); + const link = page.getByTestId("qr-auth-link"); + await expect(link).toHaveAttribute("href", /qrcode/); + expect(await link.getAttribute("href")).not.toBe(oldUrl); + const phone = await browser.newContext(); + try { + await (await phone.newPage()).goto(oldUrl!); + await expect(page).toHaveURL(/auth\/login/); + await expect(page.getByRole("dialog")).toBeVisible(); + } finally { + await phone.close(); + } +}); + +test("status failure offers recovery without showing an unusable QR", async ({ + page, +}) => { + await page.route("**/auth/status?**", (route) => route.abort()); + await page.goto("/auth/login#login"); + await page.getByRole("button", { name: "IP", exact: true }).click(); + await expect( + page.getByText("Connection interrupted", { exact: true }), + ).toBeVisible(); + await expect(page.getByTestId("qr-auth-link")).toHaveCount(0); + await page.unroute("**/auth/status?**"); + await page + .getByRole("button", { name: "Create new QR code", exact: true }) + .click(); + await expect(page.getByTestId("qr-auth-link")).toHaveAttribute( + "href", + /qrcode/, + ); +}); + +test("results show the full userinfo immediately and copy the original JSON", async ({ + page, + browser, + context, +}) => { + await context.grantPermissions(["clipboard-read", "clipboard-write"]); + await page.goto("/auth/login#login"); + await page.getByRole("button", { name: "IP", exact: true }).click(); + const url = await page.getByTestId("qr-auth-link").getAttribute("href"); + const phone = await browser.newContext(); + try { + await (await phone.newPage()).goto(url!); + } finally { + await phone.close(); + } + await expect(page).toHaveURL(/\/user\/info$/); + await expect(page.locator("pre")).toBeVisible(); + await expect(page.locator("pre")).toContainText("+12025550123"); + await expect(page.locator("pre")).toContainText("browser-user"); + await page.getByRole("button", { name: "Copy JSON", exact: true }).click(); + await expect(page.getByRole("status")).toContainText("JSON copied"); + expect( + JSON.parse(await page.evaluate(() => navigator.clipboard.readText())), + ).toEqual({ + sub: "browser-user", + phone_number: "+12025550123", + phone_number_verified: "true", + }); + await page.evaluate(() => { + Object.defineProperty(navigator, "clipboard", { + configurable: true, + value: { + writeText: async () => { + throw new Error("Clipboard denied"); + }, + }, + }); + }); + await page.getByRole("button", { name: "Copy JSON", exact: true }).click(); + await expect(page.getByRole("status")).toContainText("copy it manually"); +}); + +test("cancelling a pending SIM credential unlocks retry and an old attempt cannot unlock the new one", async ({ + page, +}) => { + await page.addInitScript(() => { + (window as any).credentialRequests = []; + Object.defineProperty(navigator.credentials, "get", { + configurable: true, + value: () => + new Promise((resolve) => + (window as any).credentialRequests.push(resolve), + ), + }); + }); + await page.goto("/auth/login#login"); + const sim = page.getByRole("button", { name: "SIM", exact: true }); + await sim.dblclick(); + await expect + .poll(() => page.evaluate(() => (window as any).credentialRequests.length)) + .toBe(1); + await expect(sim).toHaveAttribute("aria-busy", "true"); + await page.getByRole("button", { name: "Cancel", exact: true }).click(); + await expect(sim).toBeEnabled(); + await sim.click(); + await expect + .poll(() => page.evaluate(() => (window as any).credentialRequests.length)) + .toBe(2); + await page.evaluate(() => (window as any).credentialRequests[0](null)); + await expect(sim).toBeDisabled(); + await expect(sim).toHaveAttribute("aria-busy", "true"); + await page.evaluate(() => (window as any).credentialRequests[1](null)); + await expect(sim).toBeEnabled(); +}); diff --git a/showcase-next/e2e/visual.spec.ts b/e2e/visual.spec.ts similarity index 100% rename from showcase-next/e2e/visual.spec.ts rename to e2e/visual.spec.ts diff --git a/showcase-next/eslint.config.mjs b/eslint.config.mjs similarity index 89% rename from showcase-next/eslint.config.mjs rename to eslint.config.mjs index 0fc0d1c..4fc822f 100644 --- a/showcase-next/eslint.config.mjs +++ b/eslint.config.mjs @@ -6,6 +6,8 @@ export default defineConfig([ ...ts, globalIgnores([ ".next/**", + ".superpowers/**", + ".local-backups/**", "next-env.d.ts", "test-results/**", "playwright-report/**", diff --git a/showcase-next/next-env.d.ts b/next-env.d.ts similarity index 100% rename from showcase-next/next-env.d.ts rename to next-env.d.ts diff --git a/next.config.ts b/next.config.ts new file mode 100644 index 0000000..a24d156 --- /dev/null +++ b/next.config.ts @@ -0,0 +1,20 @@ +import type { NextConfig } from "next"; +import { networkInterfaces } from "node:os"; +import { fileURLToPath } from "node:url"; +const config: NextConfig = { + // Phones reach the custom dev server through a LAN IP, not localhost. + // Next validates HMR origins against this list; keep it to exact local hosts. + allowedDevOrigins: [ + "127.0.0.1", + "[::1]", + ...Object.values(networkInterfaces()) + .flatMap((addresses) => addresses || []) + .filter((address) => address.family === "IPv4") + .map((address) => address.address), + ], + serverExternalPackages: ["config", "geoip-lite"], + poweredByHeader: false, + logging: { incomingRequests: false }, + turbopack: { root: fileURLToPath(new URL(".", import.meta.url)) }, +}; +export default config; diff --git a/showcase-next/package-lock.json b/package-lock.json similarity index 96% rename from showcase-next/package-lock.json rename to package-lock.json index fefbbbb..dfc6d45 100644 --- a/showcase-next/package-lock.json +++ b/package-lock.json @@ -13,8 +13,6 @@ "@radix-ui/react-select": "^2.4.1", "@radix-ui/react-slot": "^1.5.0", "@radix-ui/react-tabs": "^1.2.1", - "@socket.io/redis-adapter": "^8.3.0", - "@socket.io/redis-emitter": "^5.1.0", "axios": "^1.21.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -33,8 +31,6 @@ "react-dom": "^19.3.1", "redis": "^6.3.0", "server-only": "^0.0.1", - "socket.io": "^4.8.4", - "socket.io-client": "^4.8.4", "tailwind-merge": "^3.7.0" }, "devDependencies": { @@ -3386,40 +3382,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", - "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", - "license": "MIT" - }, - "node_modules/@socket.io/redis-adapter": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@socket.io/redis-adapter/-/redis-adapter-8.3.0.tgz", - "integrity": "sha512-ly0cra+48hDmChxmIpnESKrc94LjRL80TEmZVscuQ/WWkRP81nNj8W8cCGMqbI4L6NCuAaPRSzZF1a9GlAxxnA==", - "license": "MIT", - "dependencies": { - "debug": "~4.3.1", - "notepack.io": "~3.0.1", - "uid2": "1.0.0" - }, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "socket.io-adapter": "^2.5.4" - } - }, - "node_modules/@socket.io/redis-emitter": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@socket.io/redis-emitter/-/redis-emitter-5.1.0.tgz", - "integrity": "sha512-QQUFPBq6JX7JIuM/X1811ymKlAfwufnQ8w6G2/59Jaqp09hdF1GJ/+e8eo/XdcmT0TqkvcSa2TT98ggTXa5QYw==", - "license": "MIT", - "dependencies": { - "debug": "~4.3.1", - "notepack.io": "~3.0.1", - "socket.io-parser": "~4.2.1" - } - }, "node_modules/@swc/helpers": { "version": "0.5.23", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", @@ -3741,15 +3703,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/cors": { - "version": "2.8.19", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", - "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -3807,6 +3760,7 @@ "version": "26.6.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz", "integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==", + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~8.9.0" @@ -3839,15 +3793,6 @@ "@types/react": "^19.3.0" } }, - "node_modules/@types/ws": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.2.tgz", - "integrity": "sha512-67MQl+fpWKVTT1NYdnmo3U4sc/xPo/zQBncVnI74qmQa0z/b+1g6iYqNmGCPbxO+zz2aklb08a0oHfegiVd0/w==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.72.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.72.1.tgz", @@ -4604,19 +4549,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/acorn": { "version": "8.19.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", @@ -5265,32 +5197,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cors": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -5518,82 +5424,6 @@ "dev": true, "license": "MIT" }, - "node_modules/engine.io": { - "version": "6.6.11", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.11.tgz", - "integrity": "sha512-z34mGpLoY/82ZbkX1LqGMIaf3wjVMPL6gVC681QDqnmzJ26synI8qAE32P+a7UazmpJZ8UGrFIeAavgRfXzYrg==", - "license": "MIT", - "dependencies": { - "@types/cors": "^2.8.12", - "@types/node": ">=10.0.0", - "@types/ws": "^8.5.12", - "accepts": "~1.3.4", - "cookie": "~0.7.2", - "cors": "~2.8.5", - "debug": "~4.4.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.21.0" - }, - "engines": { - "node": ">=10.2.0" - } - }, - "node_modules/engine.io-client": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.7.tgz", - "integrity": "sha512-GyAvmPA7gnRCpDQbYACAy79Lg4ewevrmaPzySZz0OVvgyZc3gEfLLqPTYk8wVBScjOEpM9ZO4KryyFpVItjGEA==", - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.4.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.21.0", - "xmlhttprequest-ssl": "~2.1.1" - } - }, - "node_modules/engine.io-client/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/engine.io-parser": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", - "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/engine.io/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/enhanced-resolve": { "version": "5.26.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.26.0.tgz", @@ -8180,15 +8010,6 @@ "dev": true, "license": "MIT" }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/next": { "version": "16.4.3", "resolved": "https://registry.npmjs.org/next/-/next-16.4.3.tgz", @@ -8319,16 +8140,11 @@ "node": ">=18" } }, - "node_modules/notepack.io": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/notepack.io/-/notepack.io-3.0.1.tgz", - "integrity": "sha512-TKC/8zH5pXIAMVQio2TvVDTtPRX+DJPHDqjRbxogtFiByHyzKmy96RA0JtCQJ+WouyyL4A10xomQzgbUT+1jCg==", - "license": "MIT" - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9411,129 +9227,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/socket.io": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.4.tgz", - "integrity": "sha512-9ZV9zg8vN8hO3Y25DhR7nUkJ1VDnBhq5O/JrXhFT9s6+F0gqytSASTHVfCFc2mJXaTfARroErplXlzFWwETZqQ==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.4", - "cors": "~2.8.5", - "debug": "~4.4.1", - "engine.io": "~6.6.0", - "socket.io-adapter": "~2.5.2", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.2.0" - } - }, - "node_modules/socket.io-adapter": { - "version": "2.5.8", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.8.tgz", - "integrity": "sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw==", - "license": "MIT", - "dependencies": { - "debug": "~4.4.1", - "ws": "~8.21.0" - } - }, - "node_modules/socket.io-adapter/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io-client": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.4.tgz", - "integrity": "sha512-7wjKAvVXYOJsbmUgkWbnhPD4bFpkqrUR0++XOjpGFmqDqsqV1CI93VTraeExLxpjxEqrWgFoSRZu1ihjLWfkAA==", - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.4.1", - "engine.io-client": "~6.6.1", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-client/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io-parser": { - "version": "4.2.7", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.7.tgz", - "integrity": "sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg==", - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.4.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-parser/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/source-map-js": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", @@ -10051,15 +9744,6 @@ "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/uid2": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/uid2/-/uid2-1.0.0.tgz", - "integrity": "sha512-+I6aJUv63YAcY9n4mQreLUt0d4lvwkkopDNmpomkAUz0fAkEMV9pRWxN0EjhW1YfRhcuyHg2v3mwddCDW1+LFQ==", - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -10083,6 +9767,7 @@ "version": "8.9.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "dev": true, "license": "MIT" }, "node_modules/unrs-resolver": { @@ -10207,15 +9892,6 @@ } } }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/vite": { "version": "8.3.5", "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.5.tgz", @@ -10867,35 +10543,6 @@ "node": ">=0.10.0" } }, - "node_modules/ws": { - "version": "8.21.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", - "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xmlhttprequest-ssl": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", - "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/showcase-next/package.json b/package.json similarity index 92% rename from showcase-next/package.json rename to package.json index 64840bf..1577c82 100644 --- a/showcase-next/package.json +++ b/package.json @@ -23,8 +23,6 @@ "@radix-ui/react-select": "^2.4.1", "@radix-ui/react-slot": "^1.5.0", "@radix-ui/react-tabs": "^1.2.1", - "@socket.io/redis-adapter": "^8.3.0", - "@socket.io/redis-emitter": "^5.1.0", "axios": "^1.21.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -43,8 +41,6 @@ "react-dom": "^19.3.1", "redis": "^6.3.0", "server-only": "^0.0.1", - "socket.io": "^4.8.4", - "socket.io-client": "^4.8.4", "tailwind-merge": "^3.7.0" }, "devDependencies": { diff --git a/showcase-next/playwright.config.ts b/playwright.config.ts similarity index 56% rename from showcase-next/playwright.config.ts rename to playwright.config.ts index d8331b1..642eb3d 100644 --- a/showcase-next/playwright.config.ts +++ b/playwright.config.ts @@ -6,10 +6,13 @@ export default defineConfig({ workers: 1, use: { baseURL: `http://127.0.0.1:${port}`, trace: "retain-on-failure" }, projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }], - webServer: { - command: "node tests/helpers/e2e-server.mjs", - url: `http://127.0.0.1:${port}/auth/login`, - reuseExistingServer: false, - timeout: 120000, - }, + webServer: + process.env.E2E_REUSE_SERVER === "1" + ? undefined + : { + command: "node tests/helpers/e2e-server.mjs", + url: `http://127.0.0.1:${port}/auth/login`, + reuseExistingServer: false, + timeout: 120000, + }, }); diff --git a/showcase-next/postcss.config.mjs b/postcss.config.mjs similarity index 100% rename from showcase-next/postcss.config.mjs rename to postcss.config.mjs diff --git a/public/fonts/INTER-LICENSE.txt b/public/fonts/INTER-LICENSE.txt new file mode 100644 index 0000000..9b2ca37 --- /dev/null +++ b/public/fonts/INTER-LICENSE.txt @@ -0,0 +1,92 @@ +Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION AND CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/public/fonts/Inter-Bold.woff2 b/public/fonts/Inter-Bold.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..b9e3cb3b1fde34010b001d0a069b6a5ce2a08d0f GIT binary patch literal 114840 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 z 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

LO{JFkRaobgc*GOUNWuFO7PN}S?aWqZ(|*W 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 NH~uRS8S9q;{U4!vH7x)D literal 0 HcmV?d00001 diff --git a/public/fonts/Inter-Medium.woff2 b/public/fonts/Inter-Medium.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..fdfdcc699fc1e19eb1943c2896e8d66e17b538ff GIT binary patch literal 114348 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

8xHZHepPN#0KgU{FP@5lMu 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 Q|3zqFp7#i%;`h*l0SMlYXs3emQm!N_Vh) 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 Pl?f(p%*Rm#2=M;_zqO9b literal 0 HcmV?d00001 diff --git a/public/fonts/Inter-Regular.woff2 b/public/fonts/Inter-Regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..2bcd222ecfae996d035ff72bf70672305cc29261 GIT binary patch literal 111268 zcmV)KK)SzoPew8T0RR910kWh34FCWD1pqVv0kTK{1OP$+00000000000000000000 z0000QhzuKrjtm@uzE%cc0D;OZ3W%2whWvX0HUcCA+hhydXaEEt1&(|Nf`A2EQ-rkJ zD#o_Uy>beuBfSrebG)EIk)~RimChlR+(Zi$L7n@@B@)OO>%d8B6Va;t*D;0(V*p~7 z@1q=uCHG&$7UIx3HWv!yp8yt0_W%F?|NsC0|NsC0|NsBrQ1YKgH~-w5mwVqx!U!WE zQ)DPpDT-sZYKNo!?T0{N9h`#?n!v>`wW$zEy+sCr^C*=o)f}}@XBrmDG!@Detzfyf z;sBj4dX1K#*B>}{&_OG76Eb83hod8E(dNWJhaFdIl8fuq>A-6?Avq(&GGe~SN=#*H zvsSr9{H)5UvbnHUq6<4dW=~4iVonx5N|zBN?9y0`T}cUbA!|z(jPy2cc)VVoCJD7l z^o(rlt}NTq>I-(>ZjQ8DIu0}Lyn`;XIWG9U#OD|VbJ;2eoQ<8WU7b?BUOU{--Ha{Y z`tFTJN(GLUsCTFn+1=Y7Gx86QoE#~qRpstK|GB>6>}WgnD(x#L6*4*rmikKbh|$~%kf;wglU zpOlB=7f#ij<(2hCh1-_U{6g)2vGi0wtMEds0dKxv|Lne!ziUgn`?dF+Ksebcr;vzk z^4mZInNbgK;F)-`@Hl`F5uwsmb}raM$Vk|wut6XYQhDlc2I6xJnH|YUh}dS&qZDSc zTW%le4Z=>aiiu?oV)1V9-wZjLR^Ijw7CP-=B`sVPYX|=!-Z_EXIQ{aWu*6?px!a-ozm(f)ItxguNH83}H*c^;jE-Tx4Wn1!q*cg34A&B6L7*KG>|p-@+QIBq)jo zRv*zN2KD>xij0WOGTK7fF{QxDh?Rz^x4&Oop&6({wc5jUoE?X-rRl;ldrKDNkLVm1 zbZO0v*iNsXYm@WPAn z*WZ1dFnNQuV!3zYpUumbK(wpZS_{NAp7xGh;+`rAF9A!3UioY@fwyB^P&-Bg8Oo?5 zMb1@JHo_dn?mF6)OvtPGfyq#Pj%H)?KPp_4?`v$^{2+cmiDfLGMbOw-dQOGDQHfrr z=VKyD#LSNx^W0QY;lvU}U!X*6U(`ZK`$`#skdAswhPz72EwPN&GG8%p&#ve7b58<7 z{H~i^`Y%mu3uN$o8lMT9PH(d;Lxv=Fa|V=*yZQMv3J`=yL?YIhG7YmKI>yw3Yd>gX zav}wBQQVCFFnRg-qsVkHc1y1g&)e&T{NbCx5MtI#FQ(D#RtcfVLBJ?ah4Ki!0`K zod!uO&129IxqiRjzac^79wg$GMABquhSOMdXBOIhi0XNm+{L`%m5bUpPnJaEvye6m zn}L?AnApRMi%~p(_`G}Oyz;LHtTqa^LZy;av4AG{0Yl+XfCE^m4Z++xDn+8iK&g-r z5k-m0ku_kzMlRTZ5i6<|Y*dUY0Yz*g4Nx)a%{S`rr?p3K|0TKn|6j>&C+uWeEspJexUA=|~*0@gL4x*epM@GmVZpQP4TwW`x@Mot+UWqLjW46p&_ zhBp%cD08miMj6YgPFnT|U&V2pzQ#nxWSXU;WRkP-3lY!0D}w(|S39q;(yQH?ROmd~ zsn8%kXVqY*)86|P`m3w{d*Flkq~vArT_krNEZ9mG3uoFL^y$Tee6hwaeqtA2fANYn zK2Z7D_{OJ3U(Hzczof6szE`*}Q6L3EBRujHp7Iz?;RME>n^SA&oex`;Y^r0GU7NH*LH3(8I6;0Hgb zK}^9M4auL^tCBq@i|Wius)^vlj%wQL7Gx$PCp+{0d%p8+q*_R3_GHh$Js5m{h{3RM+WGhWso%6Wq3!O@ zl&FHoOh%BFPXwaIpLe~k*ir2hy@+XOTUhJY`dh$<`DS>i_WLJK&vf_P1-8I;G=6~J)Kj<@C7;TO3dGCWv zObYq04mLU|6r)MiRxR6n+^>6Muo|V26jr97V!ZCzo%`I7)dPrbVDV=Pb^?Hb!bs=^ z0E2)kE@E#$$g{UCYuhA!5Evu^)fQz4d35&J**J4rwMG0X?dY7jkmh~}(zQGw9$Irb zpP2o}v<MC!^kiuWHN$vp$Kt9hA>T3(3QPV=z;_Tg+riQHC8NG8j1y^R3U*L(n=!< zDG&k)B!mP)3t#|g3SFeC*eYvXU0d5?UAH#3|DN}^%Tn_Z1tSn3v%T?1u6zC3QC&r7 z8zA3Epzk}){8N@CC$}IjfMZQqt4n7lzc5$^S^x;%F$r01l^q}{lu0X%5%QFLm#Vcu z9}^)14>7_OZD%?7=YOuWKj)Vq&)tlgl_TR_viN7FkV%&QlxVhU2~>pyv{fp*uD}k# z5(!x$D^zWTaTT_*1l5|^+thS;go_gg!o*3SM+qb>f_h=!{{L^P^`-R)VN_tNXGLiY zrSHVG(s=cnSmqn+qFtZ8cW)J5z4xksG5`q{ND2Z;H3&*-x>ie+x(5Y-Qjt_^L8`k) z(m3{5b${<@dd`T{SZcK1n#{OpegCyRoBeRJ-5}d=b7#_E!wtfpZInBor0YM~G+WQf zI;5sa(;NU-mAzl9ck+6zyC?uuahpGXW@jy}C4(SDC1Ba4sJb16)|bW#U={fP|7oxE zZr7*O!t&Tm`T%NV2%n_L}gr<5D^yDT{lY+K|IcXb7x~2e1ZVc>e$Q zHf^bQyBW-rk%i`Z5qF2o_j!~aifb|04-YQSXWDwl&*DU_MK zUD~I7N~r_qAS4or^CO+%e{5>CGxEY%OIp_M9_+Gm=o}Rl02LLz{*T5mBLzl~+(-_{ zEG>@gg|Wh-*oqyHLj*H&VC>LHA+jCkJtgd95ugL=u1tAl2>^9{UA+YGJT7bc$Yw(? z&iwa7$vAK}m2#y!J)utS)UXPNNz?zN)=a9D=2SZ$01^Og>6$%GTm5c^m>ZTdH z##sC+YEBiJ=Ll>$$Ww}(2}0*1ZLBC&oG(S~_-6qh48y}D5Q>NYmhbGl31#1R5ppD@ z2}hTDhd$Y%3b0D%eqLJjy|v_R(UM)~w2UNW)gZ}eh6XJt#w!vFM1fIDEn9K%5g#SJV^LKh zif|+x#maL<6X5`003nv+1h9DmPyKl#{ zb_=YJfi@Oi9O9N;sr_O5oG1+A`E23h8*=d^7@Waal;!RG5!b7#(&$-96(s2DP<@{L_|b_gb-YD zrOZCQUbDOR`afBjKZ4o49c;U&y+lNclwpt|gwNv@ABB#sm{gFd_FXn!6kQ|DV~V-tV{f z_ue{Iva*t7BuVD|Gr|xyC!Of*pi)j8A>u`;SewVgY|(D%rfv!@8)D6IZ;2)uqfj=? zeTA$h9)Uz47J`@`YwZ6$GoV@Z)Fv(^PKY?M*#6}o#%8VJcD0L|3X)1`qhNdva@zlQ zNBhR^+kLJzR#75SLZ&3beRw**FL)=p_r3c$=A)HST11Hw5v@gQYmA7oW*TN!kjF=l zO*4luzUJ$~2&IW(7|j3we(;OuhkKGSNf+}hHUnYNIr~E4OLK+r`0MBTzK+(8jEL^; z3unC2ge0L!(j)-^9c1qR3*G-E5C}5>_n>z~5h}?KB2uAPcovq@E1*B^LH{|_Hu_U;u$&5XoRT*v1j0`*`L?O;0nU=8-cS{#G*xCR^W2)5uKY$Y(*U0krQ zl;Aqu;CAF$1W3YwC=Ey|0>&N%?7a`1;}5vX2E3I5f%<_^NFWj!$l?ZaEdq7E15NpX z=0ZSw3E+1I&>I1xk#-dN1+7B^gng(Skq2Z< zeE0()B7qpmK-#fDiBh0OJa}OwN{A8xW5f3sI&VLSLVd*oaLZ$!N zTnmX$kJNth6_8t9M=M#gY5u9rqPyG~Bh0DK}?lw=)dYqU`jtco;=T-6Nl=+RwIRFG0IEXYcIRi0OJEAvTW<=Ax@YW$V@+k~TGAu9Im5jr7JDaR`Zo z(BZOiXnXhVePZV}@t%A?*>uxG8laK`5z1~cv#{!-5>ok1A^-Syx2pV72KJA6nF?Sb zdm+#)EH3A0$>PX!L>fZlB6Sm9(U4_v$~<_T^5DE+yzeX&%2#X%m61*{ydNAtavyS36BNi>u45F2t zxm3w|P3xsMYQGI`nGuFDkiCNOkLs~~N-gb=?RJ+dYj7FNJY;?;ra>9}_+NWa& zi1*<2eNopb1zIEGbSn29C=xWa1q8*lXgZ$~r2|sqQ*s^`5;k(POWllojtR)3 zFD89mUA`atQoFH+wn-xF47DBzubB`{{=U{EjX3JWpDC( z=?lw1gBQRI=8d6pzCvG2iRsly>@SkHt?r$XrsAfMqtP^F?TJvDi&j3iI;Ff_ATh>a zZt+x{vWTVzIfl2#bJ2ZAU#!{2tm&|y^xgJfO0*u$xDTerhO(q#)t;W2)v z)D2S}b;)&`jwW!=TE=&9-(GTh&occFp!Pdwrj%6alHT5#di3E#SP#~PTzrglKlix$ zFks$gM#8<@IV;g1SwSm{bI$1|c;&~Iyp?g-nPaIp=;GU0skkrV2?jlzv1fBHmOr|x zRJx4mJ?N$a@wx7r4aOfO=s)Xrq8@x@Z)~&|FXGB0&BkHd2I$;3MW{>8QV-MZ-gQRo zd!%zaW!-m9`{?Xh>7goPAsYNBKbOJin_O~Rmi+%V3}#emQlOHj_Vmxn+FuGz{1pV# zWC%?B{d5^7-KM7SL0i&u9e^x)59a;Q;1{%}^{^LN@EKhL{n9Nd9Q1^v;5lC5EpF$7 zn|%s_`9-Y)dq$1(<#7aa#}CaRKk;T=YEjEt(@&l2RGZpqe=hqv1QFb5j^%jmC!M2f z^q4)$I$tHg`MNZ?nj872{Qcne@?JRB+zO_Dtsmi2|1|`~UvdPH;Zg~1@G=*BmEU+g z@Q4t;v~bI^i`HhV_P{=HpY^e)|M_ddp&aa18l{!XMi4#39Z6ADgyPe+&2?kj+U3_E zcF-+*OUq;E8U{xMT{HuIY_-sb)MgWNy+*T_cIEgvltCv9R%h#?Wc@ zRc|MBSx-$o5(I3#qnYS4T9#fA5D*Y-X#;7$t#WUjdfa&K#-;JWGb#N1JFat4;@*&l9^^`cCjQ(ah3$ID8;gprg|2GWM7<3oD1 z1T$)@v2@co^Gx)xSw)~Iz0Q;|Fo*ciO?&??#+=Z4EsUJ7Bu699L_sJ@ebq_O+FF~A zj#Tu`8^9RK7)&walun;8#{#P+n@Q=nri@6PsckK~qO0+A)eK^uaD6@>-Td`Rw3i`3 zD($Qu9Z6Tx1APoI%*e}GIy_Z>mA+tpb{` z!t91817qW}Mz)cX5-{GR9OFyAVEjoPCa}yCJa3N}yYFcUOf z_L7!M^8Xh1dT=-j{bMZ#i=V^ljws9Xwln_L#xj{^U2hyj^c>LmTSIpRGmrEGICW zMCs23=lG4EXxRB1FgOIZYkTabu0^^>HG3>3h-zw;VIQ7$En*|VS|TZWtv#r*D;VT~ z9te*yq8K4wQ!_Q;GvmeDk6ZQ9>H>}l%>*epcE>I&yX4q9wkEYiaMwsq+_#!bFgl_ZIVO!0$B>tSP#8~2>XLle$_ou3R20 zZ@`zQl>7S6c&@Hl`^~--m6&@8ug3g)Td?rn=N8WfOQWv6z0<7qwWvhfn|d?a-$Fsx zTgLCVite|8p0@?c+e7a=K;Jt;|9=F~z<(sr;7tpm+e6fNDy1$0kt;Gf&-E)&>%shv?Yg`1@+v~a2Vo`onW=%}$S-C7 z37AB7+IpCBrk%EhW@&>Ydi~JbKCwJT)KdFI*GJ8gk2V~ZTJCGj!s z8gH_F-;o^<$@WagCotGTM?^!~un{p08yRcJ(?KB=x0(~7EI5kFQl9Ecz0*vK2Ct&k zdM-PQ;x|tf_PBz#1BjH2r-jOVX)g=>Azq&RSMZ8qUia%!{`%f9!W(^)s^9dxCdEWN zk?iA1ayIuwlcBzz)}6I;=Px?$WRDw2{3>pwz?=n9m|W#ndGvDE#Kh$G{N-EnwGzos zNmNf-I{(b|B4IQ{DpU``R=C^L=rs)627E;`rBzj<48ep@0|T+9fmG`!kfX&wX{1Co z4r-_^$=xc|N05Ht{Ywp5J)Zf{lV)Y3ndJA5p`cI0$G(0-Vc*(Q)Sohxp7Rs#f%Bx^ zJ$U{SH}{6vJI$r;Okv{U*m}Z|{fa3bJ$$CM6d;yD%hV{QQTGYGd9O9vu5D9fgG2U7 zUOseGKD_Su*j4)Mi?&0boy-me2>86?*29C;X72v-p>8Vr`S@ZPIi|{nl49GoQ`>LD z{mmb~!oPGPP=wlxaQGo6ozOrFono}?=0Yg$coRwVfyA#kk`##BP(HjlUYFV}T(<9A zd`$eRFsU{aCv#6p@(CzCo0W%(s*Y;C6JMC+Dhn*K#Pw6lu#SF-CT^a3!RzR?W#V(< zQB2~u0T4u_9J!q)e&1{%F>t^MSCA&S{Sw#22dB7*DVNd3iAy-AGPy)Scb4u9%u*P# zyj0_QF+$O zP_J|l!9giCeW(^0RA`0XKQMUFQNDfi6G~AfQ5v79!H!{x6eIfRqcwVDm(1I!Utrq% zG89rmIaT~DHL&}d3tjRCder)jg4EMw7p-*AP2b9mw3~phN0T6W=jCHeGR+)|EVBlH z-#=c)gJhZ;P@C+q&mliK<-DLQPqj% z8A%`oWYF3uduxTh!73mJnj{xKb9diPr4ze;n7y^@e(b@e0S1tNco`6*UZTBWz^9ni zNeTs$dvUg+(@0^N)PiJ?SvEQ4kzXN26jw?aE?Uwmj{cZQQ(9-u!A`gPL-139 z&|$wYS=zMJWkrXDXgS_xuM-YLA?&EXUVi|6tf?ftD2YsY>Lr8Bs_``$>PZYL&R*f` zyV_BUAMN0S!4p?EB9Tt#f-77D^MHRKtUMrl=942uJRc(7n$q(|ic}|*hBT)wo$0}i z<<7Et54xEJZU52CgJ6a1V);-;GM=f-WS*;rNurc89wxWu23g5^wz8Xp9Oa~-8_sfp z3kAbNw2$U0x0>q*-V^NiLtWtWDYAk$Q8(m6o(op!LKPNj0xlF>UI_D%vB3NhQb6N} zAXA3E7cc|wTjTeH#Vf8)zIy_mz$fNavh8IOZ$1PcoVgOfOH5p7r{;69IWCJ3bc*{V z{t#nn86`@|iW((z0J>1Ibw07tQ11(#p3)c&A1EVr)*~igx}#v^K_3Txq1>`S_ok3t z?u&i0m#e&UV1ebPsxtQs_N=6<#yYXDUVX%= z#`RU-WzM!kZ+?eZcCPA20eNd(JGo?36+aol02?#r=MhvJR$N|3OuC(6TMT9OrbPWJ z3Nu&~xIW^-vy6=#_KQeCH;!^kAwF;@WD1?~OnIS5THid15d02Ko4i%6-S}SH)Yf*i zyL}nH1RLulM4j$j7rWfe{_Gm0eR^%3?)PvNy7aW?z3ffzP5i8KQw^+6KNePI`L!)T(^g^2n~3 z6|ksZ$_5cI3)K2vo<-iVCtSUC{3B&{JmsulTAFRp= z_52`s+mN-~v{&_6-A%5f=^nSMtRv-p0Q)Moc0i1I0gtyZuhWofR1=-3h!XEx{78P) zU0iS_uL9gEfso!{zdQ#upb-I<0u$_=(7ruJaY2C+5=$nfG_Xmxi*W#F^dhv&RfKIK zj1|3tVqnK9s{)*};?%pU5Bp)$uzT@wx$>93^`o_RI>PT8{TgoM&vYt2ogDFNJH6t+ zowsqJ4F8vYbH4uKTXsd+3bnUsXhgrg=|}Qk;ufWy5dD??yL>mjM8bck zpfGQ{`1J0YTG#Rl*%X*<>|o)E1R`k|t8ZaD030Ry7p2>bt3`wxD!@~S93p>-z#~fi zMxLcu>IE&sGB4Y5LLbP%LWJr!jjXuQsg-e3$GmyhrAVn&5*3MhXstA^y9R5tCf!-H zwOFgQEmn7DS-l6{#!YJ-wxC<*2zK>RfH5kzBoG$rrRF{8)#BdVU}NuZ?(gn_d+?q~ zg+yU@7CU86MZo$_1U&B8-hy}sI$IJlsr0TBr>)t#ZQPb^-!2p0a~R&yw9?M*w*aFX zr{5Rw*!~_E{UZa$P@zL7p*w4B zYH#m=Q$mkmAM298hCk@_hk_p@TWvJlAU+R{bW0*quMU}tDez(kLT7zH(lf1mUPwG4 z3`?T2i6l9&T2LMB>r!oEYejD|$z-&n(X4=Ql*oSf@s7hZH=fex0=NvdJ5I6^Jn8ca zT>;Yv2AtT*9l-XJE=}d+(l`bgKgF(4KD9Y!>Zf@+9o#Mt&%W|`PNzTU!dwn-JzVYM zr)PeaXLCZZq(6_PM_fl7qh94ePkEqpIlxXFDbfR)l7Wy`oAHCy4%P#QpN7DPXvP>o zP(a-Sn^guX*Cz#L!U0j=<~2Tx}Ef3Uhegh=|7Lp;Ei8;O+5&B{S&^?`5;BLm*( zrSWAOGQ2AsFbEU5L>{{|a#0!lBv*1D)X_qBELLAuT8#(`j4;C_$Vx0c+&=D;K6oFn zi+3=#LpW@E&={~pa<4`n&e$;D85jI>2Ouuc73wU4z|Cr5#V_*iP4Vy!7eI_M0XD!* zwH?_oixgZUEBFBNvRuC?;wyWsSLt=zdUV?i=T5 zr59j>w+{ro{tfTkP+bwdiDM5ZwKqW-(aX!|^#WFUrx5_Cld3E>8^>p*vlEl#ZlY*s*Cljt#2Kcx} zp(6_KjrsP+5hMemFq|wh=VkK&+2eXH1pMRUxtCX=00$sy6&rtE(Dl}?E!%usXPu`!nDh8%%*`T}Y5 z3-F6(iJSp7117eh5G*S9bVF^x!-se1sa=8+m#mbfDP0-MQg#-wVg)K(*@{-8-p2Ss zy;rpwx3UL@)ph(9vKj}vG(dWDb=dM?T*)nNBq?-`;GanP3p zmG5a7NFR}jcYU?qpc23l;*yw3oZOnjUb07U!ofZP6rd9*GEgR`_?AYbkpne`lJJGb z)3ZAGj>GZ=5~w69g+3qybX9~YQE}#Fd8akNDjQNt#f+5jEmf^ILINFN7U~3x3=D(x zxG&MuuBk90c9E-OSk6 zzfTBs5}iU{4TH|2bLjjLjdT>F9_T?1uNB8UwsDSU{L_uD&_rgX;++k<5?#%HQfW

K*PVE6ivnGn>VOzO1!RVmpU9e_>zr7xl&dx%qSV2f*Y0fYWePvl>5C7GERUtbqP* zTq`7v8m$;lk%E6+w8?aH&18|!giE2jQfmvqlu4vEk<4vMrcMj83|dseYeku$H8sjM zG^pG2-K>3W$PR7dy7Z9Nl>mto@ti7eU7gdz1<=vL*85o2Edq7G=tL zCQxtLNm?~&(=Me$Ct*6>cIh)OX27D137b}!vMpxDzSL~}=$Qf0vx0%1P@qGi`}rf=_^zd=fPJw*@1jw$--V4pB{D5QC_t4MAvYA;^Ixh(R|P zVu2e=tOP#%4+n7Juz*}R0%@Q_n1BLl{z202AhQM~_~iHK#w9%hpY%d~(qDp$tdW2u zvK5ez?4T!dq*I)cGbBc?9yG=!0x^|dS{4``Dky~2vzG^j!ZG^5qnRrdemDbTiX}>^%dfRG8jrV7h(l+sp7)9jHa54p)i7u8ZBzFWo9my zp(qJCiAWvn%G*t-K`e)q6w^jtCMPeG$|9L&HQNdtIZc{1Y5pv3(`|97=4dJ)nksIW z%XB`AqYD7-nTKF#V)R)&8iq0TtC>zgXTLgIKk{xzdHv1k4P`|KEK@cI_|tME@-lIA zC2u8H;z=cyjhLtj##3tK_BtP~2-^J{MKS1XHuyNjQFpCe$N9uo7&fYuw+|0Y7#6_5 z8VC?fJkJlT9IY^hQ9qHF(K4xAKau=glk|-~cqC`9lff})Qe6h>GVoaUfM=$$XlweS@c!Qi-PU2%UTdt(Ftd(OKudteDIm)OqJ z=qlI1d=fl=Eg^Ee4WH0*wzHZIWILO+xH2$=9p~SHzgzCx!_T~wMgYc};{Nd4ivo&^ z3%(Dlm#a*Y@rTrV<1YH6euV44fL=@q-7?3|8-B;0?B&3cR7(aCyT8>p6_!1sz{-$+6hQBYYLRSc|6jIJHul zg^%s&a0y#(hwfaRH6b173uC1eIl$+7JFHXvkU(j0hUeEEFUJwS`a+*SE97C_6KsTS z9dvk8fdTHyLeO(75l;O_vj%)+w82>xf7aC&m6o@QMlR|@z8&-;G~*l;S{LQAN>_LY zgKcNOgUN?0<^=0a0x7CGTyt< zoaaMs;bG_}{$eHSnk~>1UTp=iiN&lEE{TsI?vm>g)J6hA;V$w4U2v^+5WH=-0w<;1 zp(SiwEIcqc&71};&R6rxv$vKHTwe6r?P}#2pm@U_S3j4KMb~b%%_o4ZnavLow}Mu^ z7BpL@K)9PsxU0O0`(ZzIHozBP2{DA@<)@IG=I{QND~h?))CEgW+*r2GrJge1u`-mo zQJqk^;4RN_Dce#l5yfvPYOzDf#%EJpj`Z7$GL~5JZj+2p_AtM_C|TvsKh18EY8i1? zW<|R)OWqZTlUtG{qUm5?2i;dq|UN7v97J z*S8mvs6*5UIZdxcca$>fn$j-0b8^8Hp}86IGH+dLa$6xyi;Pl zHg7d&uo=y+UTp;nn^?>$!=+21H?)&wPGD zVLa$VQ+iwI!75=2d?Fdt0H?>N$Q{KUrDQJIIlQCVSu8v)!}{PAOUuI2m|{Lk!=e)N zQNqduC3kNBgSTkP6o#4yn(lNXgwi`>S1rX+y;Jl!y+4zll!B9{oE?RdM$aA zPP0%ceHGp0f+}=_Yj`9i!}Z$6;UHTRtRI+<`d z*@d{E>iMyf=me(^OcE<5(1cO5Nev@uh}lHKJ47_8o7>XdF!Y9qlq7T@M|OGh*>GCO z2@y{s&fW|Bg;%fY^y8EIi?~e4kV^_Vl=mLFMo1N0Q4t8Q{oRs81jq{LHh}N=QAA2+ zU(#Mmi;>wNV=t>2J1{L`oK8#}j3axKbDgTHk<#3px@)V+OO@U}XhIXpUwX?ns+H1Q z%fO8p23M_27xR{kB$J^jLwK90K_=W)KIcu`cl+te(Nba;&swQU>~N4py824Uta#MF znCDNFHl}cuJ@VWEJtc7j7w>s-LD>l`L8Plr%b!JF8=mG0UWPf+?LQu=0IG$==#CJ( z@<&eC%9a@ko6ps`BEOZz^D#e+&kJ5v@rpMzeZ8qv%QKhi#7fT46fqw#PmjZT6t$cY zlUboe=0O$fhp}_&%IO@D;-+1z74KZ6-L&0~+?6&tmz!xVtmF($EoSG|UK=ag)oRJR z!V-6GN42n%UGwXZWjOs9W3l_elTsjdI2*!UqaR{M@ZBsb?;-bT!}GDIhEqD6{l^&E zu?X+l7l86MSKBS33zI03Vm%e_t~j@aAow7mH?g;j5%=hOEkLkWf`ob?+>LOprD*{C z`EoLn$pl0j;0xsAT)X@0ku1{zY$g}$psgr|{~5NMsiICLz-RxfjBV^x0&7`)1ww^^ z3pT%(^F(-cR#UytP&j^_B$*&hh;sA}Is518w#epx*uScD(^*_6&AVc5l>s}gR-TWik zbv4bN@Ak?RE0WD5VzK>^dtwLsOV%kU#e%9pQ8l0s(XL>zEbbvmEQDD!!|V` zJxgmJN%KT}JpzkXq2ztGw}H{ntEUdQ*xt)I7~+Hk=iY-{8tuj@d7v#Z}VQ|Lb^HEPLETw4KDo{J%5no&A(jHRDa4S zH*-CGP^h?N`utxOi3r*8UohZ+7x-9k1a`+VPuA_;soT2Cl70~RmPe94Y#N8QNdHNE$*|O zUxsJdv?kh*mWD}3V;b)AHZ1=-K1XWS+*_~H`(QQHy-#8-$C45&LE8Twd+ASQ3^)|d zgGEp9JRwfK9?jS@fL{jJJ>b)Zj){J!D)}##x>~X*2>K4~&l8smL?%So)SPEh_k+gE z%IeY!TKI;pC`JnZh zbpk6TBd^%C3FFOR`^zC9?ownVpf)2EyaO5ZxFh? zp(uW~E|&wZfQ?xQoSfW?8VqZy6V2%Acq||ZzWKMYhCN@%Kko80hm{4OH)8nF$%OLZ zFT!`!U7x?VPGkFCqlQsOWE?>|myEM5X*m#T8Y+2d2}0QMI7Cpu)6gvRsCl;coi4u# zXG8W8J{;S%H;Hq*xF6krI+1%;Q_6>|(vU?racz%7LGHk@rDw~6PyX|FN;#Pl;U9~S zUT%E|Msf9sjdw{QBGC9o%qtkAYE!bmhQO!S)z-cm?zY_ZmhQRPT(x-1Ddmp2U;Pp1 zllR=GSS&BnwQlX{XFDuZ31v`n@02Oo;F0z?-kPF}%ps$Mq-bzt?%b}{w~XlpDQ8tS zy~us5-}O<~cgkk|n|%whiK+~@ikgbiU(2U3q|+ARIbYOc zMF1paqSH#WEQQQ1v7@q=pE72@hTX5W?M*UzRINpX3Oi$)1vq!vE6RSEg_8PSz3iLF zES8Ed%NFR|X(;x7;gH(C6q+0MT|&o*LikQ{7@wYu85N%yu0`7srB9gWKj~z%wt-xy z+e5jK8oFMSgDO^!rv|?m4%cY;qYgIT?MA~Vexa#o6MVH?Zn)HhHgizqL@!|Z4m1nJ z1I}-KSt_M{KdNE6XOpfEQuo4OTN{`1eHAs>Go>G&ODCp>n+n3tE`Gj(d%s4UCD`5b zgCnb!I`y!-P(KoCKaO3;uv;M+`cXb$mU?R=_6bvyKWn8R>ED^Wfx zo1K*h1>wP($2_AlT_sU=Yk@toWRu-hv@kZQ`Z=p>Wf1W1^tVhe?!?M>WC%ZG^eC4h zTU=-gF1J12&x%h>d?Jt4tD{8>FAxNCXbI*fnOMSW0EmCW=6~zpJa~FZVaI1EoM~Ea zbG`7DxE`{(OysITm9`%QE;$R43UN6C-bK1&MX%$TlYm2}8u` zh!iZu4{yUD0&^jlk@SDsP`XMS$)#rLaPhsRx4%@fk%ylhfA=6RntNBMQ(2+>T$$YA zgQ?h?vvLxuM;O~xzn&5ZDprX-{+l4(hPGka|N8?PqBNzm*Rvz=;b1(QIfNK)AsoR=nT>H7Bk$8fXK?{YdcfX1u&;DEqz3ZObv)bn+ zCfuJt{4SKC){o!=jsM&F4ME4dk*CG|?|K><{d5{+iL<1_wB?A4%B6H)m$_n+$bI`l z=Ta4#^=FlOa3H?D%K75g zQKag3p&)lbFP((d+gHSWuGy`e-u+&yXTId#(eF+?wZMPiA970H&6PgHwJ3eaQ>uRX zvc~*(K^o1s7xzlce_iE1_Sj32ZK?TtCsBJyChD8doYlUSCZep%&F>cTzopzpMYB`e z%Vf$&m&bD(TC3W(HZQ&tx$ipq(S-#~0_DnoyPRJzW9AN&Y+dOS9q>VS%tQ>yGe=6)fv$^2?s z5K9d7L#-v$)?)P&MD#Vj0cWO@Z(A&_hV%c&u_h-Gw0OcgCs+w@rONpsXb`B`ZP(m2 zD9Mg3nZ7#k)!)TH&BVD^Q+dY&T6?-Z!tCsLK_p?_O%_$Yau|Dx?V>)SkL~H5>UUW_ ztX@XY3VJg!tjniKVFmP^Tq#r9t7XPvlbpHi8awG*_E5%NOVzkH(lzg`Ow7HLXK0^Q zdN#U#jDvi{csQq!0Jl_=+lo0HhxjGr~UOES?iQTa^8}no60%{ z{pJL}znvBKA6Ml4&vo0a%&gmkk=>)QcPIbEch4*d4y;k(ARIHyfMdlq5Ttmtf$PB?Nr$DnkpxO)8Gs;O}9a(<({Ii?%>`1T#1m3xWe8&N#6| zOa&s1VTxrGdj0?O4EMgbA@dDz0Ke%ozGC*E~J$z7GS-fafo96J`iwW z`9?)VfGN$Ok?D|pt)_m_7L1@3+hfa3p<` z1XHVG7s&Gg%Vp|09;l9F$OPcFskb{b9E>+v>-&KC{A(TfZ2F3UGkbmeJsHS*&4aZgh&rjSmP6sKpUQjO410IB_J_24Ky@JVdN$7u$EMF|tDm z8%N7g*rG!iWl>^n;+Jw((Dhqh+g`?;-+LIBqA5(b##x-GbT!A1(nP~QY&c!-XiMGB zs0T9ikor7z=Y8Jhje@re&mjwE7vfJ~Cp#lR-|(&so}z%wn<;Xo1*f@tsv5b8d5~4h zkq{15Hi%i(k6Jk%Rf@#05sE?V$yxQMEkyM~Qb0A!CYmyNM!{F8Ft{bEGudv~E)*<6 zJD=n<8O|e^tk^OeEo!vdc$?1l$dz%q3?!JGW^=X-COYqSIGk$+g-ExNs{NrWS}hKkW4l{PXCq4w5z> z5N|@*f+Q$|#aUBL0%(jXj)iD+laIVGjKGi^m_O5PcjUT70f|J3azV2-t(`Lx3sBs$KpfX4gV!Y2`O}y!1s_5+= z24_Km%2`(oc?%j&g+qlB&Q}JvlB;*E-oaGj5*5ApMAe>7%lIH+_)tNoog2cibf#M& zqnn4EjEVhft$AIoE&0C6`pmoSP$m?R)cx2p#O3`6>Vv+61x4A%1Qt&=K#@!#mP`Y8 zIxdOh15LIzYT`8l z;3~Jdxa>X{lTj#tAVi~n8H|SdUfU4~h9HD$8l#prLKLbnVPc4oVyHqkLPJe1D3jNI z8GQUAy4X@$a#fcUwp$WGomRnHxu;e$Ay>0u#|C|kN==-2Sv<)qrfexA(uXC|m*aP{ z=7@I9#Ajn%b71fL0TA+` z%~vj(aTKx*5B1|tNUZ!wbbyUVus3y$(`5MOh9)8tdlFkbn`zy~8m}~bB9AKR+?1=In+MpZ#jz$K|{tVBN_ainQ&MHBwxM-RZ2PlUMB$RenqY7Ud`sAxSMQ@q>bD2V!SE)O4;Tsx z=>k-tsY)VAULx^lylEc|7}vdAc2Zwag5NNt*k&r^*qDr7!5_(&87+=FhO}x!lM^ifc0F8UP)KlW zCDq}n@++JKxBW@b0KOIF_x&j9-20u!)Ed(rn``aLm75h3V;k^lj9`cZo2wyELI5Nv z=zhSdWz?*toO_EC+2N%l#ep1NMSp`x~67)dn0%5JhL?SIr?X=(0(m!wNlbr8@K1QP-rD=7>Q9WU@$k+@UyuqXnU&={debYnS> ze$_`ibt^1}_>TJk^-u z==)wRY_^z^g~g^BG= zf#4FyKCD%&W^C&O(BiZWYyt?d=VA-Vd0YmMDV~SRNNQtFM>PSrwz3O6nO?8Hw~37@Wa6YB6(}P9$a^ zbd4nlC(`1K4+~eP3bn3b(L(6ip4YnKc%QL8tFpFsZXz2zl`ENZspIjV03l?+@UYvi zH(0KlKZt2%SR?R&wQhMk+z3%BLWO`7R3pi@OYN-{3{Wi;8s&ScIGA#1rOMx zo!$hyfU2Ff%s_qWzQvZHwS&)S;_AZ#1{)kg;Bsw$?J9B=y!^#{`fQf(=_MYH zin=$nI@HcmxGUkj5%sELtQkqapP<;JsK88p8Tw4T~VbJOIF~2M-QuY%nNg z^h1moG@@NJ6wC5CVJz7#G&`%hazouP zOjrRq;*eJP;V;v#I|Ao25&PMK17niX9V!^lAwY7Ej6MsK^fYVXt?i(tt)hpG#I{Fb z$c`r*Cu>RlZk*==RR?6R^=>`|nD~VP+dW*5pMU0Qy6#RBr^-I1mKJjg$)o zXP0~5@EXj$RoyKlY(cUFx(g6~%s|Gc(Vb+u_ zbk5iY|3l%GhwL*)(G~P%`_%@>!6Tl8eWiLWZ@lGcldRI0$<-R>Nm14#zq!9Z1KwFIj3o$3~4{0CB-M;Fki;hf2hCFdG9KOaC zdCa9Z2Ecb=2_OpOTpuVpRm}p1#7rL?GrMJx{o6t`9`64+A7&9CTT}jg*k#gh?tPH_ zVnH9IvqL<4Qp98og~W4#hdE^_0I5FiIt%*zj?>sHQQd8TevWGY+9M8Ze?E?}#4bV( z<0~{tE-}|pBk|&)e}H;v$<^~{!7(GTgVaN!mnKJFl-)=nR{@tO?XNJCE)?M{`kraY zwU`p06~6#{Crr3S2&Ty|N8WY5L2=|J0s5r82M3My`tATBUDRHyTk@llyWEs==(l+jK?nKFrVO*tTCg+zEeJ{l3 zqQv*h5B8qoJycT?(zKgcbC}+N)Le7u4s>XuJ?_ZH)B25wWuOB#Cr4oBg!D?n`{Fpq zPF&S9W#KzOy(KAS`l^)|CFk!ilk6WjUyiaQ3T^-{e(j`;GK~UkVDMcpcA4Jbm{S?v zmG`hFR^)0P*41Y-V^(G7@_Cu`hNE*?UfBR3U=VMp)HL>^GoKQvJwM7KWY{y!lnGBg zx!{308R#LR`f!lBXiLfT_Hbh!lN1mczj4mK5Gh_4++a&TIb<}m&t)bxVKww;X_4%} zIy4c5a1PHz+9(oQ@PUr~axz|(PVa>n-;Op)@Cxq!yYjRNFPh7o5`R%wMOYaBEO20$ zLsPNVS`?4*!a}qlM%qCFlxdYLqf$aKvXHtzuu_-vj?UciU9yt2O@P7$b5I9oyd+gz z2+`W1Gj(heX8wFr#`oPIan^ZZgBTkcpU^5eWw8rMzJIl@#kE-{@=}*Wt}hPTCi&oP zk{x1HPt}QvHUtLg#!nyyZKd8TFLuKcFPru4jo}DBA>eK;C6K3PQ|*Vk5qFBox0OrsRf&6W?hUNr@LFVfo0e0Fvtf8! zRZa9PU!&=*Jn#ZomuFT?{Z25yP6WP28;f-=X0HGI?Xlob9Rh5e?_Lp2REx6gIUT{w z05t`L_XyZYPMeS-Gx=l=B%Z(QQN$7F2MP?5*Ph^_X*ycBYnO5tBW#>$u93H3gYM4u zntEjyBnL@GO}8dXmqg3%Bn?A0juZX%fq4alt|OJ(J(O<}TVDe^@|*FRC9BB6xA^SS zz_ZHxToOF7$_NZ8Y}71fP*u?U^3e7|>`BW;P?02yb=gS}BEK0Y&cd6ND-PusKZ#cz zPfkiZNf|!XdWZWT5yC!%Jz=$)AKS`4ljfiD~;tEqPA zq*B=|rf92ir)slUtZ*qm;r!a6iBarNJomNF(E)4;U5C(4YyD6VIU{H|_v?~No4vCF zV*Sfu=Z7V;-)&E?js`a$5y3iBkANDQ3z5j8dRxqPXrd4#VM+g^61-fHpB5C+(Nb}o zPa(HtygT7tNN?0}kf<(lue)ubMR?nk> zVmZV|zH<@9=s+!r$S`C)9Z{%l;uJlMEACwTAL|68Y&JbR6|dZN-I2(mB1DEcqOMyA zhS9o6`zqBBXJ{NXhcPqI1|C8rhIs@mNtS66@8PF9ht1B(+lE@}g#yHeB54JF_E|g< zVEyGywpiv-E^*t=**^J{U&ei(K@ZUz^h%H-rzT*(<-M{RO# zH|S8*j1?^}i9=1X>V(~*8c5OSvow#6;<-h*3H_s@p&!wSuZPy>r1sS&qJ{8sBg_v> z2tMsSS1AO)2<9bY!+1Q2Kl$!mlVPXu0*PV-5{Du-l8K3YWQfz!r+ugRj;5Idd}j8h znhqmkYLjgX_3$O?PW^Dq#!hhHRhi1oF0p#PYY%2X&I5{JpX39x)aa6}vV0}DroU6!W~zKK!^?3{Si$-NKlv(6l@t~6soUyR}=#k zL`WpJa9o#(kYB*4y-^QvJ{ByRi=i_0Kv0PwlI4cUBLqf!O;|R&O0jg(-yFD%oykNx z2|G(Ne@kl-Yuph6|5z+G^Q{mmmD1^Su@=*g4BP3*c$)q4g0S@~!u7fnq-2=m@?fN^ zfDBTG=_+DwX95F%5K=3%W$NnnH?c6PUnQ^VX}#4o*6;P8ker+{Iavmpy4w0$dOEvn zTN|aDb>~WSg7wxf(!%>{kH$^(gITo+#{=>DI zdn|m~uYG-BXMpH>JJ<@v(l>hLb`cjDzt75h=E$#6Z80%_l)K;7K9|?`t6;tOx!5B!cVfkUHL=R{bGEW+0~}%qMzUe?iF&p)O?5>fs91C zg7EI~aE0d!J+2SSC(6c*B~2J>p;@6-uq&;AF}RM7hb~Pe2(%WzudrHYbR8IDYC~_i zm(=%hT~ToJ1%s(pSTMyd0KlnWAVjflxN^#%EFTIX_1*u{6bB$}^B81TUfwDq5Gsa{ zF+na%b?iD1YrX%bE&H44HrWl9)(rdt&XXhtMWc?LF>GAJtCggs;i^%rT*9f5x6ju? z*ql|dw@)UL$(4%|UFP&lhnqc5t}qois^5s_8*t$+p*ar)c^d|*HDVfAO_yQYCQ$dXPD<|!Z{5+eyauFu63l~Pv?o;PjPw9VY!7o`Fs!V+T2 zCZy7)O_(@>o4gAWu28mau3m9fBF*hVhQKmnJ^@v^5c^L1YN_1rP!OcUY78DwhdA>7 zeRa0c0h1bLNd^iJ2SLj@NO5U9P|aycmE@*!L+mVxLpa>F)9(kpsJ?MC!tHW#uhW)W z0t_AE7A6Wosfv@+Z`#16o50x!%Sel$K$r*&lOlfV;OP^nNSgXnPadjVvI1~zZEu%Z zZw(%BO~UqHG^7M4en$_dV|f_2wE&w>Y$fC z5}B3~ym->Oaf_u}IBF!sVKVocBJay3#{do@=vb3k`W?`0U8(MwUah%9|4Vg zxEg@I66IQ1x1{e|GWhu@u3WKZmS^*d7b~eDVHlrJMVgA7*Pk<&5HAilX~f465i_2E zidnnmaepu^5g;fOBoqRLPOcj=Cj2k_hw;>c({iz7Jy$N(gi%utIK$dtY-qlmtB`5? z)&2H}%Lpj1|JDi?9;6Hj{T~aPP$MKHCMqgdEzyqEcJ+GtnKM?-bVx3n$!t4P&Gk^L zT@M&%TczW9+=6B3e#t}Re>-Zr@q0Z_+j{CND9X1`q{)oy2j0aJ?2AK9c(xK8#wx>r z^-@3rn-@z~jXz1QnpqNDw4FCqcvYe`iFucbrz*`DFFap)s5KQz9`09eim_l>OcgR& zJ(B_@oI{T5W7~i1DEF^I?-!zzUC~-$RM6=^rL+2HTA0LbG_>^k zv-~7_uk=&u)%E)6)e<_@Qj~Q0)aKCPeKXFgw=c*Ul+EuZI$lV%A?v%I&_xr7#)?J7@EOxo2YBi@7TczKV z1!~%?0fo!|(3s_>Y;|n3**GLD=>xAzhtdf`Ji1me&tL6}E0a1h%FFRsmA*hnsMe@$ z%T6A%h>sfA2g8nEmtN8CV%IV0p#W`LieoSDOghW;jJ&u-vH6VTa6KF#Y80cnWt%5E z$={f#R848j=+b6&d>0pZM}h{@8{spxyY})IiX$tH3>RH% zLG}0NNH((gy&j6K5kD83D~pMymg1ZlZ7DXLyQQnr+%Km#*h*7(m^z;-vCTEFI}=>8 z*;Xwpxy?r9&4z88S&1gd3T|(}@9TQ;oHqNOTCY~gg7u5a8aj(Me%q!Lx0W37#phWb zjVgmei2`4O4ai`-kz{rPqZ3>Ke!%V*^uYUcFRk!b1VdJ-P+_b*7LcipnJ*l?muMBB ztzh!t6_WktTF2~-pFeZ4{grU#W)-+*G^Mz8J(5&xQyyuME$F^@>+jn5EpIT-_^IqH z+`TTn=6a}Z+=vMN{Hs#!aBTc`y=UIqA<^#)%BF2QsT=dux|_F}!tF@c>FKXeCZ;BE z&e2r)ukS6}liSnJe|Xd87@Y7-TJI4mhbS5|e_8*YMlhIC_(4lkN-=NJ{uB6Hcdj`g zPf=3NqMT@6(v7E3w$N)Ub^J|cDW&1HkXBM{N~hyFA+y8bdhPmU_^y{^_^1;6AJ?R% z#J0^yx9QlS^au-pLb0O4Ie+{y{+%C3w%)%7coYc6_}TN6t3|1(ak#_SWofT}obW=8 zL>>Pp1RTx)yUo!5--0=++s{+q)oa$9%{JrG?zp)SY!NO!g_6;i4Ouhj)U(XpIj>Fs zZ_kLwK$%S4B3_jg6eT@DU1@cpy}|v*>kT4Ie1secFvjJ+{n97$oA-HW3=AZc=nRfL zdV2qUijKCX+Sca!>T~JQ!GkywP7&ZTVim@g37hPb{~fd9`GZ!lW?QLF}tC0yj|yCJvjZ_S$HR>X$vB#LmMNwchEgTCC- zJ>%t&q7y~8o$z6x;U`7OUL-QhWJ#3As4H3F*Vzy!MA6Ec&tLomC=JO%7=vTzp{LuF zO>6a!$TgJH$TAf)MX($x;J7Utis&8fYY_jFs{Qu~^#AG!{%`MpCrSQj1*80D&;bLe zJ`#s#_p13?Z*%ABsy3T$g-G>(r~h~QhXexLw+_??B>2C>guvNm9J2vU|LF^20TKQP z)&n6v?bnrZ~X)sa1@tdtDc56Trb!Fk8(*jnV+}LmJRJRPcO^22lw5$ z1bpsh{$@s|kD_W=*x1-P2gnGk4ohbNKk;NgW4!~offe@12$(soCUU*eXn!`ijQWo< z&gouE6TO4eJlPDrvtuYt+sn0lvs$&U6(l17V>#Os7K2W!)p$A6754|T7H>Mt3pQ^V z(j_hAI9db1i=%32G@XZ;nxlzJVbixt!*`FUJ#~A-|32 z7y?L%77At9n%@=*&==0vClZY}tPB0GU0%{&G!lbE{XcIknP=4!h1rB!)ty!iLrBeI%GeJSNTj&7!zsI)N^YWw zmn$e#F;QMu#FXM6vupQ zmfAi7Z+B4xLSQc_8o+>RMz{MWn;*!I- zrwJ9*95@{*bR9O^?53#f_Lp1egwR9Oq{t{0&x2^9dXK_#Q$91cTeBkFyLDXd9w7pC zkv~4_Ue)B& z%jB#M1(L(Sp`Tuu;`9n?BrkMR(_i`Ch|6ot#)lQUv9Vgtn!yE|*>+5eDHewn&l7e9PXsg=VcGy?ByKzGlsgg$$-M^KRt78dG z`jj?Gl;rwHV}JU!od-Kwvz)9Su{JLGmu9hA5?CuG#%m$8pclZv5#1Q#_mi1LP@?bq zWOH9KYiRZ`HIKHAJ^fPN4=YH)kXu$6LX7o*bH|B*wLOS8-Lo}fTE*|f!upBU@9@gW zC`}Uz+@=*dcvyd3%c}#7N350$bpyjN@sNs4K}RB0g?mJ>3DXW`65$xr0S%b~4WR)Iu>uXb0D%MmfzgaHte+yKKq5ONH=pPn$szSVh z`VpnM?k{LCaf}M%W%M)`BuXoEp4!Bv?rFzXfV1XRLHH`RCx6GE{)d3P+Xrt;@B7&*0Sa*&Bn&^N~H3SZ(1PX{BO5( z|4JHSu`s-Q%KQkFO2t^5rNX?9IEYL@K`b2p5#^j1;*S*A5ER)+J@weLYV@@V-M>41 zIf?os%BMIOpEC5kqz{B&ge~Wxx1&T4TX^6)UpB^AV#1$j=i2dP4(8~PN3gr;YQ7P5 z2fGruNXKBW4FphW2*T~8$~C6JHK+BMC;N}V_QN-I$@?Q1)5gV@e@sfhHfo@awFn&d zN80CZI(pK*fdN3?44?XVAcz*49!{u!3>YxuKtHKD`wZn!EAkFZc(8&p3;qE&5}DvI zL^mp3I(zxaADT#|Behc#e+UVK$=*{@mJckQw*3=ukNV6N7jtHou?0}HNS>6h#vg9! zQ-N_6q~#SwBwhkbqu8@#M72K>FiXS9wpSU8p!$SjhZA)zrMSAmEOD@DolU}114?CG z{Q9Mqd3|cKlWYrV7ulE+Jbz$sA?y5jcbRw6={_B!F={dX zIJu<$EX06P18z^Gl_SrDKv}fcob0~Yn+b$l%~DX-^S+y5=yh%X#)Mcqo5QTNqf++R z=hpF%q2iPd)6_o`Bky%(8a}tf8T*w#!d6k`(NBt}Iu%&8zNOo;Mvob+FoZ z#+a{F>-vne2f!XvonYZN*#=KsZ~1QuukU0Ek2;y&n9Y&Js<-a)Ua(S0w_G^>zjgah zU;4pMmkMyX@{hg0LszOd>n$hQGx5K#Hkxj}gQsL*l?TIqhdKOchK;TAu@Cj-j`I{w zSWvz10$*M;C>BJi7ApHHr&K;^u~n*eQ2$~+|6L}6UMtzH)NG7i21Z6LWht!@wnL>h zE-i$EC?0!yCzQsqTk83X!vkY>L5rWp;;B04smYOjc7;Gpispm<;=aJ-?&kNgUU^wi zpK!Wwr-6mr^*RrvK=?dzfFL&tQgR#w*q41iR{oxU=E8}YQmUDnylVq}iZhYLWG*U) z!}@(bQi58=Vu4JZPp z7;2=l9)avqR7wH>&d09Kv9ZjYqQ2cL<)OX4MO4$BcDFoXXRzHj!C(3uFsD1sSF&mB zI_##+su)pOU}>!5f7H7##u`w|X0kgSpug6KO@E*i3q~y)g#Qhu$u%g|tbPC{RvlY} zGV8tkevvRle26c}{};k>5n$+!$N+^FCs@d&iHEEJW%u2yx?(EvDp5`3p?7h`h!iWt zdF?abW#zF1pqHfX46MlL&^}BZ?{(@DbB2|nvHl^XG&33=7^}g2V^k$M7x=89l3w8w z5DtWpOOM7x`)vd>da)Rhb-K*Y+P$7JwfNNtrCSHHYnsTKW+SVxyC!8$J<8gZmj3vp ziSqm?9O0!!NtT-CMJbN%N0u>0k{G6bw8bgrS>jzT^H4VSHREtT1kNo#Z`S?fU0sK9 zwl+h|^b2G03rLa|-66B9G(wO%F3!f0sR zowqzcL6=fq$uZ_Tm(VEX*1KXVmIc-#6M&E$bTQ~6aTuC6jTUx&Q$uABjK(wbcYboZ zf#63k|3B{!Ym0(8tp9?r%6WSvX_2IhVM=6cm05n)U!!RG{J$Zkb$ohCrkMb)-)dR| zX`_{t<;yT&YjVFDErq{HmhDacp#rZijaF4C`?m{}FxgIdRqM)>Z)uJ`P&@JLqW1W1 zDJa~^l%#mv^Ui4UwwPM;rcuCqco-!r*DN39+2^*jRI236SX0mb4n)G=*?-B3n)E9wIxgUH!HI#n6-7}R7;EhjvEuW;!?Sl*CH%P)OAN057c1$gr!knQDrGgE@>M(C=*t?^kC}q!9G&tuwkc&_V{obBeTKWO3yP;9vgXh!p?yQtze_Dr_{Wr4?J~pSv`|%Q^ z(M`hy0!C`^4(Fm+Nr_!asUWZ`pEQ1UA`{}LsfgRMsu6)!DSgbGbA3PR-|es!1@9X zMuwBWmR3C5-8x+0^BJ`DbG!woHzT_=!+JZOoh*jh!{I((3RkRo=6E>qc$?Yw@J(CU zD<4z$Y9da1XEiFnG77HWjIvM6wtv3+10e+=qYXkQ7GkF!f;W&&9~`<4HeaP>U$FTJrKJo@za560l|BCcuIW%eHR zifLN?V+m_}4wl0jmdZR9(^{Uc8Fyhfpw+O35?6$ykh~ywf?$Y9{7g&=EVK&q)RJ-M zn{?YLAOKVhpT^Pv!@gF}U=Ro+KKM-{Kr$Gc-}bEA;8(}Kfag_|boGn9o9&_T2)epoVMJEZ7VV0!w>mRzZ=^qj-JbqjdehqQY6!W5qY>gn- zmZfDu5;@jIVH#Va_n;@gt9VGvW;wq|4D#+=p$xWdI}TDDdVs|8BBs}gP58`+tDBYu zb`HK(ZXH`X{(YCt>VDQa+bZ8J!gHN|BrWz4b$halWc7`*kZ z$yzJ^Y$pjt-JaR#+7_eEGd|}Qls7Sw2oTyXIFKu4G=J-hKx8B=yeRQP;!dXE z;UD2BI+P5C=Q_>x7DfsbNg(XdFrfqoUJ%fZZ~7NK%KX1ZU!7FL$^aLLq~Q|>PphIG z`KqRQEYlw>-s~nHpeyc zkOy`_RIFn0OnN=q6BNRHJ)({AtP{&~bNjdbcd@y5wJg{GfX%B0`{giV2sU3Fy7Fe_ zq=seP+Mi;~`v~+NIfFug?X}j_@&)&r*%70I=$gUtR&Q)4f;#&fe6%kePZc zko$4mcfq=scP!<|%JUjoKAErqqTiaNErc9Y>6b5)zPxa0R`m3%?9|bXd!XX3AJu$4 zJ_mjOv#-eYO*k1OL9?N&=!Sc7#pJde0X@-mv5G(0w6nu<$ek2>s8H*ZZ~$Qk%Z0qbIU|D2td~ zt_VDTn_+9)&vPYm-$r%vh+~X-QCB+#Rrry`nwds0+(9Bq&#ecPMTK1_KKy0detuf$ z%i4$#ho^SwEoZ`Q-tiN4SfzdXxej1s>bXw({^omF;9-bn%Cy&PALq-xnXtQ>`E_-d z`xk{gSdfh!B|!hzu_g-V0F}tx+CQ+8KSJ@d`3q3`egR1a@lrhLA7P?igwg~@#RST; ziz?JNE$EmX$vqQ8L=Ny%r+vH^rv*aVIg%k6N(P;6f?c zcR++X-jsgAWnx=Xq-zFA@58PbFlWz+aBq~$%zwX`4FAUHdQQY-r?)h03}TtjMl|?3 zZ^sz5&UL4oWc_1snjL4jk2v5Ly*Y4y_&g@~)-p^WOFfVNq79Ca6O2bQW@4DAoVQuV zqgSXiHAKP(U1Y&%#l+L)^#KtkHbzlqc7lN5+ zU$23z#N-GqMPJL4S&U^) z?{d1uChbe0ae4s!;$E!cU+f{{hJ)J$!AtuiwO^5XKB;6vLWy`y*T5lw$&H)A?qaH& z)Uwta_{3Rijx;3%Oo~7I3Qh99k#FON=;;@`5!s)?@^wiIkU#@xD(~N0&}t^0Gz^-l zP1W+%ESDqHTT0CUYQ#)Jmn9?)FUnvAXvW==$*9{-duz?iB!a?PYDAzgk^%YDNXZ~V z_=J>@K_^JGL55VOOma}U)RSEW*7Gq+6+^wo+yZgkn3)R+e1XHMvAtR94@y50R5y5r zxZ@2N=Ryy$splH_ZN)@B*g&PGcHB+f&{lej3_C1efp4iZfJ>L6YkP$G_BRiZZl$Odd^L} zj?3_E+YjNRw>Pn&mL(l&J03S=e;InIC0@j^#sMRNaMHC^Iswv>y2r^p*$=$LU|e;T z2V;fFM^QC2RfVzqrJ9Ekoi?V?EV-|!(xi^bvKq~W7j#G4ev(JZvRo~!i&6w2fgg>a z>%GuqqWSz7@pnh+~Et#Z%{y}VDsYCU(^a4?4TtsPZ?>#$i<_0!^es!g}^Z>)>iQUgV{&sP>o zBr1av3R!Y;l6hjk1RTg{3dAHSsIcL&6w#HMvdR;bM~iUR9Yyv(BC;e=0q#DMQwUr#JCSn zt3QHz@~kcirS$WOEVg>3hkEC}bK>c9vHV6XHfXH_p6(DToBN+?;%Au;e`YkBv0Mhh zdr?XLj1PTBLtl^3Hb(zBPJA(rvg@qosSR<@*cbn#bu#g*loD7?Hn19DP*rT^$M`8q zAe)lZ?2yY?)MDEJydfZ z;iC!Byh4bu06Na6Y|Po|gea`FoSViTn^_0wyOMt#b*{Ufj`tf8J-eQ_fGXptcj5Cm1gy%Kh9g z?Z4f?*gc1QU5OheS-vzmV8Xi&6ducu(%nBY_?NREA_Z3K!en|J{XwldPF^VPwXBKW zEirc>vF6c|yc5FxEmfUqe~ICLwHU>IW|(AzBQh8F_>0~SLYi0ueUuW_3Zs9XQSFvl zj2T2!F;)RFnEgIC}LG5dZ$<#y;i}pP^qja>j~qu zI3XDLx0w?fLogV!pW#rbWsy`OYK}CNuK)F`sv(z_;PN)_=3`WdQGNaR*Hy{uyw<~u z1H)|QyJpJ^z48~L%q7wk>lvQI&LlXr{d4@a_>5%loZd$@Qd2~iGlku01f$TGu&6k^ zD7jWHCYK)Vg}9Xd(TR9)LPIQu*_N#1qmA}>?YkO;$iu|_%|pZel}^osJ!JP40RPQ% z#;y-ijIpq5_x6ncGMMxw%NzdW;;SB&f9!{UbR$C{P>JQ1y~?ho zl3M2#nsXLIhntR_z{*y8jStUu*Y-QYAK%kHLc+YxeZA?T_ma*y3iqncq@d2rZJ9fP zUGIn}NCy94^Ri#Z>cDoNO*eX?A&ZWe_wbu+r94)AWkxmNYSaI zrdCeGi#@KI>w%JLOsQ;Ja}^eKy_iduPjB7s;=|zEt!8uQb3Yo1$F6$TV-Eio>g9-{ z{*!(f!BH@DpXxJVS)66m(>O*xT9^hSpRG!k)PPsjm5F0jQ}xxuJ}(~3ukK22$)1I=~)y?x~@!h%Y1?^T4{43!Viofxf=kEi>5cU`&Sa&dDl<_ff zDy#W8A)gWVYGZ=l8$C5~9Vo(3-U|(FHCMJ(C3_KcYu7d8zjvk6Hg>S738g8t*5@T- zSW;1}Ysbk7{=I3scw84?^iBsC^v)eLuMzOoO)adUhG|zMbJ<8}%^RVk70m5pk{$_u z$m*I)4rRGD~nI=_UC8x<8qU+*%Y zULqdjDSt;k$6(b(q>#ubc$lFqNiGpXOEU+dp;c%uL=-=aF;Xlh(&9Gak8x3W(%+5W z^CC~nXP%sXZv_FFrRG9iCP8dNoa8n~{5{_M^E=@3YoP2=5r)v1Vzyg9#^Klg&Nr_g z@Bssw*ir$u+0;O#CT5nA^HqqRBM`!<7QIVCmEIvn+n?#Gt`SI|0sD`BAB={Nvps!_ z^;SJ^Bv~1=H!$WTi)T!jVAK{#G-{1aVM-L4Bdb_&sQ+M*Kq<9LNa);%OoA~;s6%j4 zi7bGRcqXGsxp+gSL?XfzV#3P#5}n_`HiM4fsHnK`xJ(xPiDsK_S2zN{&V-j)>&U=# zO~;f<`?c#C_lZemfK#aDxMPGhhg0KpD5^w7M(0ZP+J1@r5Rn@kKMEWshfu;Olr3d3 z6oP(V0)bB@Ns3SmX~V_Ov@^{3RTSK=u-!$7v(Z&zG`D^M;*x3j62jax!&kdr-yc={ zuixy>O7yQ=^c6T3Yg~9rO1_zCLF0rJ4@;Ptmu?vu(^-~tw zcJDE{-nBS7rLWMRZ)h066v_THp!@-P#$`4WU{Z-bQW?q}2}b*{`VPrVt<}p|V}LkV zmv+3qHnDgYf(Vr*0?$hc`{ODT4sKmf=y9Kdm(#ZdJ@r7C`|K4DRjk_b;~(khEd(Xrid)5#{3*c!0le;}{_ zW~KZ$kgi_FfhA`iy=?9d&<6Vtd8M2cB5CHhVGXaxP}#@IKF1PUi^~&SjO_nTb3{l? zP@&KuBnXuNa2VkfNK*fU)8o-^ZUtFbnOiO&W0OdkV*GRopMCA4Ut;{)r;L=XsgZ?#eF4 zO57S)UG^?px4ORh(45E@tTC5pa@QN6VHynM&}3Oo5wZ;Pnk`q2&J<3e9q+SHBfznt zFu+Bs4mUzCv^K9(aM)d{=|^4IA*iB^?d5)BTz+Lig-|+1Z&Sp9IIP#+9wK&V8&pP6 z@7IKnID~&9$ zp*R2I=E`#+HMWy@`uO=8`?Ze1QSeuEpZ@PT|L$h+gK&@8cCCUQR`0vdwu#;gS6>)A zzOT8G0m>u#$P8#C-FYKPXFXeI0}rGV5@ZvLfq7cGa1`@|%JDVqBxaQ+Y$v`3x)&<4@4^QlRv4zO)6;~gCoJPa2IR_2 zWW(;_cJvv~p8o@SK!v|vb%i*(C>kO5V&Qz5Krh&Hmf5O4`jMxe6eUvjB^zPcd~QBemyg(ilRt;3H7T zmZD;~;)dV!5vwm=C1^>eVUa?t)Bg`S=pgqFO65gjex1M(IWqUHMUI0Po7ucJ@~XT5zL$ra)|)xhp-#1^OTX1!^(<1NbaVsx z(x5qR2&BHO>QCAJEaR763n}b7fE&NE1JFQ7<^@LEkb=;S?LnbL#O5GGr@m{-b-v_6 zCb>X|9!iD<*mMQxEu2C_X$0*@=bZV;PljQB+UMbZHjnW09_hQR;hxg_U%-C+kY85K5x7FV;X;*ap?h zRjxtJDGuMn)~v=m1ZuZQz#aJ%p;Og0$7_XM{ACzwn;RR1g0=c!!k4j z+bO_lnTrudYr37s1z5;ZBv~VDw4_0?<{k%h6rG38P)@Al+_@GJVW%C^Iq1xEf*q|^ zV1OOCPdw_glz>@U5W9nOR!l*bGs<=@EWIOmU4u<_TFG6#l~*a?1Amf$!Q~JD2uesJ z!M&!39OP4h7FCgYf~sT{)=r3YN{AVV%crJIl2%lyP8ZU>qsR0TGP}6SwjAU5>$;<% z&1`glO=%5Uj?=gkwNf0lQJgJmZjaJY^jMlxj-FlY*`=O2ugq4XfnBns<{VPo8Ew+p zV0CGZZQZkTQ3)^HDXU#1jvhV&>0_dhxpiy;fFwy~W@gSg=bYzxo>$(r>Ha=$`F!hb zE@>A4NRnh`X6Br8&Uv2adB@v01^@s60000003>P3rBeVPNm?XHk|arzBr~&F6aYw) zbgPg>%@$-)vjtg{bI#300e~b)w+dO*+=47>Zb4Db^E_`}6aYw)bgPg>&0CN~&0A1Z zEZ_E6_5VDjz~WpurFH^WwvTjj)-69R0fz)2z$S>KZ`jQ*=m3BXKWS|Dk&4az_d=sq z!FK@6`JMn}5DfqWfC8ZVed;d;0g7%^n*QT|JO&g0;FJIY_#PbF6WQsS9bD~36YYQu z+?r}mi&}#dFHurtsWTKVMWzCHWvI3&T8n$?ZMy$b@zWhE%YaO5*~2ull4jDJK^tqh zhHd72MvnoTbDb_L_SRSY%pX;yrfa5baww!Xw9-bqq9rTK?A^=?mvq(BUF}!gX3Hyw zxlm}j8_Ao^{@?Sf9Wt_TnH!vP#v?x4)g9@06Rr2O*By4=Pn&9{wRYO;@VV`gPwfdL z5+@Dm$%)7L)I2NZeMKv zR&}En@n^69pP1Rojc2ZZ4qG~}o&1A44mJhugcfG>Pz08zJ zg3l)G)mGXY+T(?PQ)P0-WN`8s^Wy)oV}CMbU0;rexUW~sPHdQQNGeqLGw8z@GLaD% zV;V;SALxjSm~z4tc2n?$c}cz}_hnMX8?&Nmiemne&-00Cj_106@fNl~(%Sz{NUwU0 zYqX~wh4CAjyopAfL|p%_$EdvByue;Dz#g0hzraR3Orm1O7YZ%xle)a>vtI7E-PL(l z08{?&sMwys)1OcvRpkh!vy?3JZe5$Xk1O%byTKgUnWIjuzXT9-qWE`Y@bUFj~ULvu90I zCKNOaTDIt6)izpL8$PpXQ5$nKjreA--tq{VM>ZwfMjk$P+avx+?0WY;h|;b#ZgYjJ z>wDSIZxmZ+Me*w{x(>7aI1KK;M(ax-tHfHbiq2Yhu=$<;ypvzizpJcFcpv=d`I`Sf zHfB}YhjwiipFlztC)g(fMlk!v7QsI6zs1!KV_ftS@il9Xp%pFprcP5*RdQ$sGYms9 zB*QZ8*2<+Tx#m`T9d*|7_~!VSjP2Ns995Dx~3NRXvC z@e;(CFlh?=kA$|l2BT5`e z(i)-G1pQU#zg|`A)3|<38_>K#EgRChTja^2ffZNz-!=Y!-P6lCwxsH8ve8m^iMHx6 z8L0DY@%bTGJ<;Rm^c1;JO@H94PjPw3Q?p;XRKvi7M9St#?To6!TAJP_V^PzMpC%Tl zDRrc}s*}-h=CvDoJT;LSk9^o8PC4f6t61gSb1|fF!mK=^2A<^Ttz-p4f?nTdv5Hf1 zs2e`CjU4($4`XAe4K@~H1guzq4x!FfS)95Z^=FoQdaU-$5k7|29(UUaD_(uYPiGXD z1>lIQ>4e8)O{#(iY}ndEYB!|Sr>TD5hlN=uUN%WkU_#WCP@NA`6Jd#*WQ_*1Gqd52 zJE!Xl%h+Z8U4R;h343vhmqghmN%l#WLsB%FWmALGV|;Ti(~%8YF%+wu#C2XAGgm6- z=1#D}tZ;c<&m&Mwkom%0MLH*ccK8)QMV_9#*)RU*iU9+8G#!)?O)<3nrasp5E2m< zw6Ue5PlJ zhe{@z%6c#O-fTIVN(QL8EVB$p%|AS^Fg}Y!0YX~3W#X<~$)mf9x9M3~HCRH~_z|^^ zb+w^Q9ji|rEo*t5tt-0qZP?uw?cRD?oQo}Kf7u=GNJo1kvu$rj(>tC^t?li#Z*W7E zs&tRFZNnQ_+xquse{Js%bbGI!B<+*h^4(hJx;W&hUaKiv{rvMkW}8!LajVmX1=W~b zlq}W9UnmDX+Y?fkW|gWc8Qv&$^~0=i*j}jSf_t&rH7KFPl1eVE^fJ7)v!!;fi+<5J zUFqub`=PY%c0VTlSKTZ;pj(H~9}nfci!OCJmv@<~KKA2qfH9_yc`|w6yY6uRu^z!0 z>sn{N96Fx9?v>n;c;#Rf*$;D{UQ`50AjTNgd{$Z^=@ZalH)`@FCE z`JVr|pGT;!E&D`yWSC9w{QTEmr`u4)E-!Y&lI~`eyCu$U^SHlw+TXRjh+5yK#cgC& z8(sCrR~P>HEm|in_bK1YIj?7a^Ix2Cfy4~u3gi^U0=qQvKLzRvy=b%x6EDJ z^UaL5>y_(*1`dihZMhaiGGq+OmNRd@`id*Dr2oXudwu?ZmWINS=+j^EL^72&%x7-y z9zB^a};gO@D&I#^3NEd|UBs4Nuw`Alcv)i(|16~+n2^z}Q zRgUg*_4Hm#d3tM8U8~B8`#W|c<1jjYOB1j>K`RroI`M@NwC;iAASjN-IwgcDW19;0 zouRR_H0MZb=VwZe|dSBMiWMVd^z zrqZG5bZRDDnoYNEWI$~hRA)xil~Mi9u(~s*o{XzEQ|imK#O+T;K&V&}*CvTC*{1o9-{x(%d zf078rg2S2^`xnCAVSRLunY?5sjGw(*>lsC#eglRK`*YUJTR1Us8ZL!|4FQXLVkKJ9 z)T&dByyN+5`f|AN;AMXYKY?0MB;B&fsCPd3_D$wDtg4I-8a8UHys^&(6FYkIv5g11Y( zLBN(sWpag5ta&+u!v=R4tK>!vwrCoK8ym+XCrDGAHq5S#0O@rw2DgIvo9m8ET(Yfx`4-vj6w z?t+Oyi?<9m#5Fxs@fiSy4fo%~ZAiKt{6HrAk;{h)#fMUD)974!lh16PSS)j^b#Jqs z?e>Gi@#b{Cf6(IsUKT!5#1$psik5xFD7#{fyyA?#;w`)q9PNoiBsf{p#(N|=SuQ0- zDK%9qEzLYVJu4$4KQj}Vl~tad-H?;ho}0Uzmv=Kif3=`syRh)X8|)%O+%j45Vo-?^ z)Gjq7f|Hdk_>?QR+ZBrF7XYkski}$GOHbmUS~YJqYD7O&DN?5nS-pDA8Z;QtsL@?b znrvy-Y+s8Oe|xKm^K4jKMOwRdjyiO3*Qt}2E?r`D(+z@*427KBy6(E$)$x z0N;dq1K)fbWN(CVtrP_|cCT{NyJ@Kl_=WU;M(-uYPykAO3LFe1EC+x4%`? zKf=xc|Gou)<>X7jg=ygwBB=kO2@8 z!VW?=59Dk4` zFFN2#jR+>Q&D$qux|ziX$Fk7e;`8!u@!S|}_2MQ!0887gTKsrwc3QLesahkV$!=pb zFOE5i&0=wIEYay!H!ci-$eM1mS%@54&NjP+=yBkDb6oHrC+wT^;>2;mp}8(j9yc7D z`{LB`AhUTc?2Z?x=Di3yKA@ZLBKY`$Y5ohy3810{F5*uRgciI=I1q>}WRZA6A+@kY z(g~-rMJ!M!lBO25Kv%R^s!og;$P-I%i(6Ekc=}qxqVXis-;x$hCz*kkvS>a~23zW) z<)ksx(ig1<#;umI*gKhiuYaS+bltA9&od*Dp&sa>$-s-nP8;_E5fj z0##r{-N}y|zf(w7D_S_6VzRFi`MoqW0sIsu36_m0^NZz%d&N!X!AiTmDwGabJ)+QL z%`Oq2)iSOxUZ=-bUl~=M5id^GZ!9!u;H6QMhravnLH!WeHvAdl2DaRyIM_sw3a>8G**xVfO6Ek@e* zf_`=wZMzG`*<-9dShUqcL0AApk6xz6V~+{jw@=-H14lgZM5w2pI^~&X#yNDT(sR#E z@xlvr=OstmtHthljdOo<1Br@&%dFbULbO&z6b9EK8iMOXdM7tF+_fpNO}O>S6}D}= z1Px8H9Xsx=e{N9@+@;v}9z7cS_C^2w$K}9*=?)zdK1X7-8_eGX||8Ohd({97dS;!rQR0h_kW=5(t;s z*j}--yKr!P=H$#F5^K4*a!I5ZZf-9go^)Q`mp&@RCxD2e5dMfZ5P{$Jp9oUWgw~t< z5#~Y{8Bt-1=;m^X3I2nKhXFt&9_;f-!qlYx{R%qL-0#PNwva*2hoUKBFXxgZOi|9G zX+$h`zC?mbrM{NQ{Bm->Yi2Q7)=lzn-vB?y`F{f9lm`SxF9boiJ_-ny)I(w{gV2~| z0b#KX0pW2G0U#odM-UmOG9W5?J0SX*S;UCpDORlSP8{_;{x-@0NN}n%;{Bec@j{4I{`tBrE1k#bk*D08*Z1`0gcXGO&(v(o?b1U z^w#I>zuMl_?=q)j{O_G(g{f`Pmr4wEb@+snDNLhFVNO^d5NX3XxrWj&)pKK#WAZpZ*m`)noTrvKc_z~~b zq@iRqX=*PwO`CS^&x{qOX3e@}&b;%keinZOO-q(&)HNY_1OQzR$squ@ATa$O!Tbb% zbHA3AZxHMg7Is+_6`7QjsFjszRa9uI>cO|F=E0+`&aa_CuBl0-rA1k7FM2}f+pU?N zp0&Qdk%56u4IB8_griN=*w|xD477T}>BeMc#$Iy+AD?is30PVRSy_o#TMOIR2-@1& z*gMeG(Lj|ZJnco{{QW%!7Z+-O{y&bth^t=o4glDKU-*`SfN+F_bbyBT4D-d1goTxW zgYyax?*$RjQ&DE1_tRNWYoF4ark45Fv5@J zPfCav_Y*4A=M^q^6&e|wg}w>nfJTL-LZbs6fX2L7o!C#@kKx6OcT^?#=RH!10{20a z!WaR48~6-Oeo2ZHDW*u3O1jbnb)o6Qn5Mqlc*>B0S*A=+WXZzfgAZ~mTW|)N^BOC; zat+Fpk5C03yoNs?{U519!yKm;?RubCv6o7eNK>j*jZZ!~t4yUks#JTS#%IY@E8=hH zm&e9^zxvcg?1t7qHu^MhQsejAQljbmqbmIL)34K#S*_n+r~OZx73tcw`+PdkYMm=; ztIK2C`t6C4-?M#tY7phy_a;^S8`cILh&T)#eB!o7Jkg9A)okXp;BarQEfjKeuTYL)RAm2G}I0Z(eaB(@& zNkMfO_l=q`9!eK4)y@}`f$_h15&;7468wn#!~mjSF*KU^BKSyf@{*)YiYx=i$vf2* z9vOWUuVnTpXR)jNl^56ihb3y%t#plB%!g@OzfPiET6QmO`(5XzCNN#}t`}+p(+_cg z8H59X8HR4bjD|x@Gv3P#6DCl9Os#^MIR)o5bBrulU|`7_8=H^qj|8w|C$sED%z-(C z(Q;3E zV!YmpSP4tMkqAq%d8Jam(ropNhht7A+x`6wcfqp!?;mcs2bS&N%3;}aA2n-K{WT&@0 zIMUO~VgL1j)5cM6kC+PUi1-!Od7~7p%U=H-ascZNi~+1Cgdau;j0LPWL;%+J;F$F5 zCsG3fr^5zs5(sPBhuF5^MV&wXNb}cU^?ydJN;hg$gE3>?88@!+Okme1V-nzuvic|# zl-0+vgP~7`f@h!ZIOLgUI_tSeTYU(t_|Z_!U9zh^&hzSkGAg@Jzkvpe&f9sPax-Eu$tu*gq89Y4Qty?%Sy|AelE z{*5Ds{);UR^ndI`IJe)U{{9LR7nJq8cZNv`5h79@b4*%UdL^oQ1zWb%*s-U$b)K6s-Bo3o zHJ>_jYt0~#D`1pgXdG8OUJrrClg~#H2zU!MdTkv*pYgw6mJl7V7(S0q5tFn;myAhK zbgP)O5k1zJs*YYWrpp$6_Ly$R=%3ELoR0&_S&{@0;<79YR1_|%DpAus&~-_M;h||t zwk+N@n-sg9kHZ1#bb91+DMb|J?ylOy!{GCPo}ODoB2NK;TS3s%5ac!(_6&l!5k)xk+O#E=FQrn;GMTUBaw`nuYnHXkaen4`dxGE>g~Gm4 z=~tD?fm-c1jRx$kp+j^!$dMz%`uebMD+J+&VfqL{3`H4Wn4>t(kRXVYq;nKSf~I*h z3=Nj$!*Miup06O#5=DNJL|c}@6@`ncLTDOSU57IaVWz3bvV_~VV#g8Taw&1U?NuZF z@(Tq3DH@GRf&e8+RS-(mYE@H|G@4d}kaV3+i(apgVYIR=EXSc@49D|m_V$Vd!K5f^ zmn2iNti!>IS^A- zPu&bK)AhAHn`ew6Ar49fhT$|#A!8mm7YX6fvMks(jw9|(`w0U_oODvwPZ;tsuUDAQ#|Ru%Dpwkf8lBFK!Jy7$a%Zt}5(I=NJ=?$nIdVLeE7ztx zc@7~J2~!+|WL}7Bl&jTZk5(*O+FM_p(2p(7h*8PLVj`K7Nt4`6nG$5jo~{UJ0L*5F z^!YTKT?h5ed4gcxyfRl~F;V53>uN2>jbwwZxM`U_T_zj z``yL+6!8Ar|5dq9@g?!t|Gs(B-*GI{Ubdo z%ZTjyg{QQumU34&+{3e6v(8+#>O9i*ZrV^=Htl*Y)3QPu*ya z)m?54Yq->!)^uU7<@#3E-s>z+^}3C8z$~w~PJ{Jrpvi_dw9(7ngqGe+ai4(lcGtu4 ziz5~aHBU{|g~9AHG99uI&e^%%dHL=I1%^mu8VUtNV=@a15m;PKQAs|&3|C%(CKCNf zq#!amnnFq8?Mg+O4uE;pksQT){JSo~)vFhyL8JI?og%mcU`{>abNSKy0WlxlQzjS~ zG_`4S4il3O78X_Q+8Ogp&X@0)0tHn2qaq$C7Y-*j%S8)Py0}E?(?-~(8)5q$m}F6gquxq zTj=B&UTk&>ECk7rA=;|0o7O{^G4qcL7aQDpSQQ@$88ReFlqE@0yno$=q+$;R+gDOR_G9js{= zyI9Mfu~(*p`+6lz0S49FFqQ@l#@W=SdhKjyYdhZQ&g{R9v@i!PTub~G`9r{Bi#1qc ziJYaDs_JD*7yt~ZDWL@zI-!J{!7x9eDA#zX4E#h7@9B!y7iN%!pAH#*85uH-Tc( zv}((4sduS&#E2}TMr9i_F4u$!1tv`?G;LN1Dyk`4L7Rq_ZwMRJ5;!=^@D2ZFgAucp zg{671MZj(yS0kM*!c7DkF=GBX=9pbFp_VC@def+EnzZH7aaIdQ7ff>EjH*$)ZRx9l z-*4|1lo-z>)RXSePD~AN83S~Bt08NKjl|M_A(Y-U2r5ZGHTwsX#9xtsFqzh1p8^zDJVZpavNkMA8oOuXkJ?*IRpR{9I@cL@BsD#kW| zUVWA$pyo`aLgFGBQlTc$HzBG?G#Y2HTh^QKNR?FogFY4?4*)V>0s!had1vhQh^znE z{%2Fv0Z0KL^oV+SsN%n-fJEfwIh#>M1McNTT1tk*30WjiY3v%mk*lYQ9QF&qEx3Oj zO*pa1@upX}A|D$$cM4ogL}@CAtf(m75-lr$+h-tx8|lV?N2A<*U>#JSPI=b0?Lavb ziGccp%_w4$7U~6bth`rOy~!vFSU~8i6=k8Qs3<%Dr%cdL8csJvx@M9JoHuDtVj(d> z^{h}d=C~SR+_HRCB1IawNbZpIQ;Rg~fh2WbT)<8O1rRHa+R3PCRcxvudxA3_*sMKp z#u^1LZ)~Wz@+MRYz(GUTB|DuGUH8+2tTjRuwSOkaHxgi}2pKbh;bWBq2jdxgifvW7 zR0M=eHng=$1*H!=pb;fkEjN9JP9%m2X>Cm<_RLqIDDD56ud(tF@_B6f;f<6P;B~=1Q||A2KV~> zRvZW*&Rz2GWQ6HrwKTI%X})S}Po!R8St32l3~bGKApt3f3PtEa10JC+0`wJ*co(0( zG-PuC{*|o`XFi^OXfkLlj9@`}vV>MQ-UYHDF`2u_kwm+8%HWgxY;I^|3`9O#u&zT6 zf%ZW6UBkdAWx2^K^a6J8wYsNKI9#1{^uAGsrR7G(v#{|UY$5^2eF(+tnEYYqxhb0a z%Fdv*f=m)PHU=UqtpGe6l$J$G27k3gI#LJ#!-gW{sXR&dQW}^hnDfJ^RjNt3iyJVh zaa)(UY|JV}s)kE9rGW9+LT^L5?gB@vL22d~R|v7tr()VwFFPSNR3ier%@Q@%~bW#()&zc1c~I9-othVkaQaE{LsM&%%6B zks}6eu(t_gY~14{x}p<){@kRMp}*y|XtR!vJ?3+I_RId?jV#tQ*gYuMUA?C+rUqEd zX@8AY+2kFtTKXZC@Gu|E{vp0zU%$lm7BmHIiZS4V?hUV6TpzDj@!TI~%gp-{ATeU3 zVyB+Bz-bQ~)^K=V0y>Sp`u6YT-B+$>p~3t#wZ$#YOF>RuHD=pttFI=kNU;qL1pqSu zfZQ@-eZE=)8C2Y5lZr37>o~?r1~m>C@KLzO$0s^*M7}utn%zwAQt=7@9?_o-bbP4M z9-hOjxCy+EbXZ0dauE1HW=zi8BKOI~*$6GhWhDh{g&CK*y(ezcy+*m<_~zUOe7{Oq zIw4|%&7yy)fMuKkx^9v84^nLSwLW5R%?D$~50fGZO(Bt7KkEDAWnZwsm^=7B8^mV?#fOPNRxpx5=E-bUp1Po zREEc&=uuPrcJ)sgIK)V?7*QA`q z6$gXDu2IWRW*GIC@*WRC>>EDSv*^HDF8&n2-Ne5RFViZ2>2Xj zDuP@oaxAU;Q$JKCpELY;*;MX;tn~|%-?!z~X++ubE!gw`edN?)M9PKK3 zpCFSE<*~BiNl(@Xs$By7jS-niE2TmJkYX+T_T+5QS|WCpNO~AO0#W5BDZh`DV#XlH z>v+SIy7J9irlp2WDWX};lkzzudLefV=5rwd4p^~DBTH*2Y408U0{0Rl*(ei=1x~=y zkq`on6=M~xrU@*xo7_Oz>tvDO-duqs>$-4un5U2?I|(}($lNqmfS)f#MI>>qv^SE? z5HKMecSJD7H@JCU~+rz>Ib)vMZJ4=*v;IgjAhU?U&l*)ri zmUpB2(s7DsmndiDEG>vsd|kKUMd?IWi6x$~B&e2y#mAcTLeGd8UeJhJhF2qaLk*HY z%3t2kQ=pcLY5sS}wf(g{$#_bv9D^%e)Hy?Z#&WiF zi>z(Ku4Oiln@U|V9eIvsIfk>`HORW7Zg3?V;ZnXO=I|_8oL2L2S`>|NzZL!MiY=Gl zYiCk9Q7IFAgr#Y;^?HsZc5E0Z)ey?fBO_U;HZoFDG%)s}1;>A&pW?lptYk^xp2`VO zM~em7L~`{uVTiFXwOLu$RE=?)jUw~@wXHuPzJyuOQ zVH(gZ6cydRiHfQQ5RqUQ)a@aF`jwL;Dx_imr1!8WgsAIEJW*KFus=lt26#De?L>mp zRJtNH<^x;To>8$Be4cEzL!W#BT|TZ&66PbAAMBw9xG{u^E}mqfD4M1!RZmGZhv`&4 zg>u0l5UfX!%1b0&eaXFrQ9S1w$M^(_rS**?GJ+ehQr}3AdaZwd;mQf{71i+qFshv` zODfO&bljlAnfQdI8MEUx7JSbFpYzM`@r7^HS&0846b){t*DVuf=HZExGA4%SAGwEpqTD}}}?_VGm+a!l^FPb^5P1{!Z z&w_E7&0#=0L>W2VjTpz_l%rq^!h$68)$$X}E~WA8Do6@nLpV3r=v5j8zi z<#>XY zoNk4Ggj!;hw*&N26>LHNhE=YN$eZfC4H(|{exKr8rHs;u77KtfC;f0wa5?WR)#v^u z%Wd_R`8`fZk4UL`0heI5Jq2lF&QfBx-G2a1yrx7;n#>r|F!1B&JuyA$;_92ST2%}F zuo#n7M%b6B1!%|>sDw{NJ^_{yF+DWDzZ+LSEOXF=kl^eJMy?Uk141HGqr5@23@Zt& za=;HWhlXls;#f822-um;@Di{0D>FlVP=2ypfqr3@B6HR4ppQT#Ok1*guGkro)b5qO=)VkSUu=9nfrD`6RsDWa(;@^J!qnIwqT9s_0T%WXGQL3fE_I- zb?JB}M*AMqiL>>RLwjacOkORB+c#sX?Z$|^L2wF^m~c2fj+Z+HQ{Al%E})OBYDVGt za6%Yu_%mVA7}E-XB{r?B4Xp!ia$xoI>{}ZcuD2}oOVLp1Ndnk0ZzWNVc4XuI(?qxy z1XmWz=Ha}@JVy*%6Rs^f-;ft9{BvTU3d3+-FhbTP27>xDD@^a0phbF51TZ+}!LB13 zQ7`LJzj{(ol?F84<6Y-*ofXiRVgFqt1-LOzP#!3qdeLK_w@>peC&SvGm4-em-n6ad z3i{B}$7VFS2exmlNIq-ea0Ez2qB&X+d5+Y%62q&}=@`qJoUDDArzlb$UxyS6b`Wc1 zGv9cvDYn{&m#^?R{yMIansDbSLjKN!)Q^C(O&)xY_fC#j8Y7@DfZ5rQg=P)2I2$y; zoGJ_`TxEfG7lQ+o+1?v6I9@Lsc6?C_87iDTF%<5L(Sr+*tRegE8_B1lp#+2Lh9VVI zw3zducu--dHBLqV{s35jatvpxKm~p1qxOZNN4IH5KYP-#M-ccB!$4}4WJ7E2WowIA ziJ7r|DJBIS($&a$|I-j5YG)OYXV%?o*u-v-1%`HOG?W8Z!c__7+A7%Fap<=(uee}x6Q?SX2q_YPNA2cCRr74CAh1b(_x%d;i zSd~1|=)a^pSJr$lKemNM1_x5PC#JOYs7e3Xt=xc1*4trzo(D!&=5Wq%4D4FRWMJSL zzJTLEr+d-;LGI4~yAZjSZa(6-q>ol0KVUcWCU(1(V1q^;j4_&*EVf|0Y>0MiCr(Z? zh=)<_Ui(xNnoqS!&=R&Tev&Xx<=WJ>^cYN=#qR7Cn!D*P*|)Iv5$HE<={)8CshYC2+n$um?I*P#bq4?1>ZpSI znZ>WOoJ6Sn73fxeU%y#zqUOv+y;^{PJ)1Pb?l)mFlK*_o&n|%*MUAGnik> zF~QW14Re1mJyuL>_FUHv`*I`{8(s7J7fs$iP}=c@3*o%A0-NcpgqNi`5!e_L)dfAv zfx0;^GEm0WX|o=GPPeMtELf4Zrm|)|O6L|%xwh{V%TYi(BzwHe0mpbMmL>`DWVas( zhxX~a6&5APMY^*fc^-^D<7`(!nuWoS@&+r1P`=4E8scKq{Lh&lbgG#XU}b|7V$0TI z;eu20*5r6nBZV-6Cg;w%S7-t3z>#;F^YVX-q1%paTA6QyA68)|rz4gZ+cU^n|) zt`5gFwaRF$Iib$)*olL)&%eDuXUoBZIa6q^8Kzp5A6^cmoOzwADB<_dq9B~^*`diG z(i_%qv14rbi<+{28a6z$Y%Ql>X#wt}M4Nh-7;sciad;IpWSg~9Mj)iBKJ-(b*&guKbKtcAfd{cejB2G=gAt-SoHvGlT&fPFf;gqatF5Vu$9;=s1%Lx*Cv0nb0-r z{ouVPFSp|T`Dx&p-jj=51wJb;E zRIxW<`1z0*DXLG8$>t}zpQ@NHkyT}zDA-JpqdbqRhtuQk-eLblYr7ci49FORW8P`Y zd*pbEy`nbNStIYAL9XcPwOA`BIRH&`xX!-1106W1rP|(r7_Ubwd^wp}nv{@B`()^JCr9dX`$#}OKKZWB>wF70>!f^|*)Z!E*BTgfNd2CKC7Y~fCRl=wt*C+Z z=(fSlu?c}imQJHymgM+aO>F%;_P-TYpm$UZ&1o#}0v%ax3{*q<7(HmPkv~HViqJ{AkZRP2wsJpzOfodt+#zP)uZBtzm+pzz= z6XLONN%RVim!j0|L}F^#I+{Y;#EEBvB!)GNU#GE)K*0LX8UcSp-qiv7xwhdxW4Qg6 zXtV4r=t*fOr`#hwQTiq;(68F+VYZhab6?Vk>+ized}iiTI&R3F?lDFqsJz=bL*M0O zZu(>8n~RVV>{<@|BlKTHFL;TF9W^ybx2C+}!bSrIzPsra1Ora?^~u}9J}Pf%T`KYe zQ8w(TzJr@^*ifCDaA@wZeeplJY1n4O4D{%s(D%HZhC`WN^&-& z(na9Mk5dLRiN9UN$%L^&-QnnYeZ6WWyL6;a!Q2ASe_-Yycdwy%Kkqrgk1FzJU^(yS z>iYl<(dFmGaxC6j*N{$8?qeuAQ$5YF6AV?wZ$_@zBb1z03)&JHYoFmMnuf*Cf{>tN zOg3hJTDIj%KfRQ)Dax5^%VZx-(&p1UX@LEn$qIMI()ddK@Bm_9q8iQfjXgh%${Lhs zEp=7O)ABBP@ow8~LX`6BH)$fq~k9pN6bQhI$^yu$<@oaRqpq_$?3+TJqD zU@Ul*u5fM4An9W9c-N4Nk+LEn4vwT|-tWub)-WYs)Pz@nf?bT8;$$2`>$eav{7Ha)!J@3W_LfR(_0NK=eUpH@Lj+7?A<*IR31}^%U zw7$vd!+VQtFaYT7{W{Dy+o0@$UEtOA7Py^r)Hro-52b}KCj@Ef1pikAF?u6;VavBp zZ;J*h7>t?+ z|798!*YYPXDAmi!w8L<|3>=?2IMqAZkhQl#8r3p`(JC~WN=}NH(V1(o*cRp_BiIcy z_DOsS_De1VB9!D>Ux^p%%7+a1HyT2aZD<^vKMT8ahl&lzKO0s_>NU4~4kY#?>GJxC zY+B@@o>wSP$9pgR`)GAE=E2a7M?4&KQQ#b$2g&gwUciDgu7iD04QxKQpT#rq<5Xlf zDfjL>LY;-?KQXhkUUSy##{GX|CD1?_4eWU2hT~)~B;Fqz|FC9_z`e=a!6rBorFG<| zs-Z#23-HuugwMrxf)C2JlD4!IW11IJO=#e_gH`N`XC3G&^w2}EejhlgECHDp7TS}MNgARFkXsw<1$;CJ2Kb06(KEb-$t`0; zK1{iLUW!oXvYh&}7{?9g<=K(Qb~k14WJVJ@iUMvU`5W`n$RpH1N-iqYQCrjdb~#B^ zc=L?@`NenL<+_m6MCuC#cgla3pNBz90DMXppv7-VF-Vs;$db`nhYdNijfB(sY$f0s zGp$@e$9l+q?HYI>aIrE2IH}8VQiESC4m)eRoW|9Lx1ieU1d1mdxaeZsCi59`Qj^u#>bg z$sazX!zucNA9M@eIZtIm$Gm!<=UNIb?7PpZMf;rEy>Zm`W2z>0t!dV8UY;p3hoI9x zQ>W%6JMOFB7u(WX-hGpG-*Pui0^1yOJ!0)NC9EZ{j=&crSi|EZY(e7?4sfK1q}ZNb z;u8a2@1mo{+$TAih-@2^4sQ}B7`P7hHEM15ol^%7K4tRwvw^afG2z`Ea+AFsfgpF4 z%fwcrwWyOyJhC>jeIGDFwgGL2qNgv{~Pv~(a-^Q7M#ZN?}YDZk( zwE#W#m~w3eD>rFlF<^glns$~^7)gPW_=9QaH0$&lH1$`r z&x0Y^e*~#eRRV(^UR3RGDMPAW7;oO9AheGGC?y5&0+m%eAn)EH?yNzS6AY4}-Ed{9 zWB^K9XP_4K(G9hc8Tts8+Eq#LS7I2T-k22@oRG0CNY;tn@ zsshJ7Y3|k5$zH?K+o0IUNHpa^U(MrGH6GpMA%Ed*Bh{wszGp#YKUT<=5<@8);8@l7 z8Vx`LaOww5QUKZD^U~5%S5pkkDCZJUIXaIhH2e?AxN}ZG?zhSdJ7n&}{VtJP&Xwl-+O>TGI3Es$3 z#)FS5_30ipsg1o8rxBl2KXE*KuB{!?hnz=JFlZKW-NmJh%%_ySQ(dYE`jS;L{MV34-$MC`7*|ELH;74aNdLa^_PS6=Ik}0kH`mg0f%2- z*9>|Iu6MJ@h`W^&4i7Ka#=O8Nuc@4Q3jX`+z|M7gFYUT#B6nbF1@E*+y%Y+U>}B;TW@5ZefC*-m)&#2&nxpcl{zV}n{B&kB z8cwmZwXZ?;OP^NYrOzn-!k0Mn@<)&|J|~e8^kVF1iIya_{&?CGz(~qzM~B;l@d0kA zKPh*5i*MgnoPj=qtXq)zRJ(nErDq}~Rp}~@31AEYO|vQIFbHTUI?B?;-<9(!_P)Z5 zqvOQPfxk~|4Qo>zT`-Ycd8R^1iqXM;z`qZFpVNdjleD1WV+IQL+HlyiZbj-Hpet<6 z*oZCDfIE~c&rT>~|ILp79sgtW){064Th5y8rjHpY*o$GauG)lfit?D6Z=wftL_N&`jcrI)bxs3m-r)o}E_Ft`5>EGWDl0iG|rZ0dcxZYW89mcd@u zj6`ylq9`S4RbXkPEQDncrC52-c4&}gXbERjSjNU2=s;}uOA5O*f zi~&!C)SUH`?zE3CracfRy^un)=I_xK2jim$%E>_9(Y9j^reR)+3`XOtQ@lgCQXCx| zUUc5jaVB3Mb=DMsC6T5_b%oQBvmUkMpaFZ+$=)z&?Le&TFoRyl}+RoF7x%7$y3bfx4` z)@IxVpSpR`cGaEsLP7IjH2zV&fGv<=@z96IY?k%0`sl;sg;H&nn8$hW$mxU{R-l(RoQQP!{cmE zt47tvo<@c0tyPMVE*sdaX^A-w8t1B9IS>fh{(Evf3>7cKag$qXzk#2#95q1~)BMXz z1O3<6$JT#-_Fkr^r@NpbIV(Jha3?)HQ2&1?*qwHLf4@29^%S$h*fm0v%=&fMN7Li@ zh@SKoTJ9KB8i7Ag$UDg(l)L}@+i9j z1_C(+J3I;0#6qSGt*_B)cz!F=_X-Pako7vb1|;>fp7}*CM{TTwr!hBCJhVd$BaZQ_ zMl}ZOi1h=2XT&@K&M}Z_or4mt78p+9XcW|ss{(Wn0{Foi9Uf-&t{@o8n_6z^)$(Kd zIh@#REDlxQQiQ!awVBpi#XHY-yD-45$`jdmzpI0XmRahP1`L_*0bW39RDm-B(mrN& zTin_TaJ{tQWNxK^9@0uQnvNQlF6bz!qJVWKldWQ) z37LG+=ydxv`Jx@x&7Vo2I>Qxczm}qv7tCYnkB{P)S3jw_J=%12?JXO6W^Xd~#8?_+ zf5W9&?-hP?(qe!xUoj)pw`kR=M;QS|c+W42&A2jxd0#vWy!`Hs^5de4u!;UP9BiXa z1K2MK$EyW$T@d>KCT8>yHa}zk$W3T*T~zS|+>=Fh^_;!DRE?bw7xS)JInz-^%}&8g z*ST(B^}J6Nuh;kd!qdE|p*3LMP`UM~Km00xOp`raN*AM3*=WG=p)K%NQxTdDVjf{@ zk}GB@`6ymZ*)0yD#YStAd>j zw!=+s3`#5~bzw*aC>*I+f81L-=M_(`b){9c1OmYOE}+Sb*obTN*-Q3wtet~-8C<((69fo>mMD6x0GmlDBOPxWnjEg$-d0_) z&F`;pwX{NX+vS#SFtC zTbalHk zbc*+eR^O;w&1cQt!@cm#CyyXJdJ5rfWzxG;`&3meN*^O#8}*TWA~vWFRYsgZ(b3xRWZC@~fYjdg@AsZ(Psh&#NIc4a(#?8 zd9$Cf|APjE)N-;9+x4CWgY*WRo>{Q4znm3-&MDC_iwoY?Ql%IdQDAT_q=`VWxUF)J#mv7_dxm!QLP)lLB z-X#gEC2On}(3K|nQix?yYSS~OT50T)5rnw5V}JM**i#*rcJgB{Q1_skF}gSkx--jevqzDLD;TGiLo*ggjpRm;bv{2i}BV}jzF z|6o)x;F3_2(qD`-^b<&J0ODNQ0TR3Z7optb_K*Bn#_Q!Sjc#YFjdX1X@|fGxsrmNQ z9=((o;NSYec`V8E6pFf*Ahalk)tm1_#zp4 zgYr+Yug!2QQH5_oEH;I-el1;0TEH&*iM+>iIy?Yc{fT4K;%^10T5^R-2m@B7-X_41 z!OHML(Ao^~>!EkZMUYH>!(VTT4N;D0*Te zaxk>rEbE$r(DJ0$+}D=B2oDHx9HNH7Jq0xmt;=hZdnRNArz$g}4j)T5+_+`BK)(`- z@-IK=(WMDOvupcrw5o;XU+UF#>ibX$T6O~lC%%);(spWxpZL>(%fxxwks7zIT5RL@ z_416rCoR9%%wxHZ<92DWpX63H`c~O-ZBs@-7$cW70%pyD^mwGzs%Ijbq%_vq!wk%O z_z`89LodcO@VzE@J#b9Wu6=pB&f1cFi-MY%(x9?mp(`}9E(leB#hO} znhqhk$Eo*#%k;0UV))~hrw`xielPax=gt$GqwE-ZE^;#Fz8>tL>`xv5z8X)V7b}sY zIyd2dIpuW9XRLc}O13H*d~>;O+P2WrXvU7OP-*d$vl$me(KQJ#Z$QAUpOAHIwB^K* z3lGRM=n}ByyzPSOIbrdFM4F26HNr1`GPCCd0p_3u<-zqn%jRv-qJbIq4*>FmZgm%pFtMsHWb{$(Hy0Ks7gi)5<2Cy!`xcve4n#$wqjzW2D=s_IN zyfBjA-SDKQ9{_MsADn-rxjcim;rHL0O6~3}N-y(N>u-@GxhfMKSk{2@<8C_6NieaY z#W;23af}ftN}2hgD#u)%&e>xllz(+QP#3D1Scb{g8RATs3O)eSV<6&b%hG3+B+#yE zyaLxcZ88Hi^Qye1cOCx3O0Jj9!e{BS!v*j-;grv<``=q9$-Tvs(M*_3GA-r7{(%lbWJqY|c7oK32by7NLG#*HMAUGmHlG~L95gfbWu@MbbKKQTaRQ+qL!PbT)AKMni_uEb0=_|v~k;QbIofNeSjL-#aO zUbm^Ij5wyA+%^wf<=cM(jnjtya#rePERr?F~+$8EftZKkj1Qub0;ibj^U_5 zP&+bm&|Hc>I9|JuV-wXF66dYk%3+8Q@XiQtBZ%uw=^|9CfBE53hvyCF&M5|@diX!j z?r=NShf7Up<6=(pI2CP(*-Ogk%TuO+G%Z=!LZDnDaSAvrn52h#pF@aAHuk4c>Z9pB z78Q9iQ;YNL0VRXBu~`tEC_^ymOL1f~aM6)v*2a#%_bGJEp+2#abTNk06J`PX?Tk>T zZSQC?mQIhmcMUzJ2C1Wg#?nTVXJwMv2J*qpX^T~^S)=4Q-%kRO_KE;Gw`m9E<=ouUY6Ohul+c#axW#h_CGu>xzZdA!8Jk&Yjy54c{F|No zr}-0*l36EQ@xHPSK)kBnn=Y!gAMo$@pTq$W4z3gcGcboVGs6bv=D4|j?|-_PZ{CkR z6T!~y7kuk%K~HBW(QK+iTZ%Ee(weXpT2M+3U28)!X8c_5rx}U`hKd`j4^CS@S*~n} zCbZnJ{&-Vc-huE;m#V|C-jlHcwz!H#HMwU|wf&}>!OW+_JW(4OEBNcbrpfCSrCDx@ zq@LqD8I_1d9Gp45Oe%cCb$)F~EWj8&&AZM31`M^9R>RwGy=`u9e!taT-YPfWB4m13 zE*P)@&#GACN__Zv-PQ$JdwdTV62JOp$nojP;_J^||y<}{dBg4PbG1NR}2cIfU z4+~;RE`Vz>zyV^INOxgU1Zd5`h7voug+T_7n`b|8%xa8v`n-MD>7y||Tk{*%xg%FM zBN^ohb=iakw~F$3XJ_(xw{Y0-PFr|LGV+pKb^G4$jQguJM@N%OZV$S`+PLcDeGyvJ z-N(95Lxd{=S)Q+M2QK!j%to1>Z($}a8?xjXF)xMhFOrfaDZ=G6`MTt=Xq(%7(=u$G z>$LBk^=Xaz3049;o>YwGEJlh%hsFk2`m*dP2W|?o39VD42}hpxqtDP1(9Kg&lHLl&b$X;iZVg4RevoM95c@VcrDyy z#zgd5=X8zepeQ)0M>8p94~wHR^5&T}6P#zY)dKM7Kg7l42ckc4JUseyYf7{_7<#2> z<=l5>OFe;5Q_n=tpc&bFyGcMsdG&-}7YbZ#7eEWN-)-kd^S6Uj5=-A+Y`V?36_-|| zr*+3JR+~}V{Qrf-rTdqYVCq)WV`+b3|A&8}@{sev@0*)XUI9En>$kT?m)t)rey(xf z;-9la-(jFH%|mgUI=`@I;=x#2=w!rN|M59hF1_dfx*or`2TcY%8iVC%t%yp0F0?kC zmk)^aMP2okrgt9!=+#M2j|J~I)71+2zjB+eU-N8}I^)^l_Ffas5y$<($CIeAedVX) zk0CVu$R!C}BbUeuUG^u63vQAYrM#{S(TghhH;p{1rpL&}d0pnMY?}Fg>ALZazMOD1 z4tfHU zk9YdKdn^3vV>5l&O5mNosa9ZrKwY#21VM?Pj13;eD`4JrwR+^Id>!LK!h=nm!QbBf{KOBj*u<^J1-<6St{M+i z^N%# z>kbMgw_4aZ``eopm<6I%fgI&GA1N_gB|ou+=StMnL;{rP*`50roK^KA@;r z`gp30*ctqk#SDjM@GNST3oiyIC4@iIeoMW8)inx7*9xF3b9_-K>ob7CdiDF@!`$eH zd9AhU<$BSfqVd~rZT_>}LbEaK)pwR7zue3&O}|ww=(IB};E^-2MZPE#@YAN3*85tx zg^ZH>#Q#iqVu8;ythnFM*EnV#?B-CpvZf2J8;10k)4}!W%KW0K7coT2K@5733pyd3 zIGl+k^%cjtXw{8|kY2S};(1JvS4V7i&9#rIfGF4fF*W~McC4@i6GS{eTd9`ZU>K@q zTaFf!u)~?ci4)vV)Zjr3h4><-Xez(*`g97Lo682=1veR$qjHa#M@jn(!;2StE(lK? ze+DWn7B+7!M70JzD&^RT$T^Ed<}W@P|ay+UdNQ!0R^lk>(4$BsF1pfJ{j+*DwqaS}}GoHI=AsL`?~#z; z>4fE(tvbSg+$C`p$7-@s*cK;4h^5wZ>fLvRe(X%|Cg2lkGihSiE~c}@9#I;ApPQHO z5KPvJ{930)hi5d zyD9@&IYeHR!>&FQbHw zZZ`~xnQyhL`p_~tORv@^I5H)KiC*yMhzr~GTb%9 z0Y&Gf5yE*Q^Xj~@8Rf33=GgjfV~wb;v_4%QScQC{tIsgzV4zl5#gpH3{z`LXiLl;B?IGIm$W4pe|U3gMYSOZ)+vuauE#pdU7i*Z*IWTkKAWB7g77&LmMfbz zDMfp1G&rjB_D)PtA}f4OdL7YH$^WsXm%N>B6gE^4cId)qhD}$TnukE<7l9%TEzUMR z;@H;|P)Z#x4lVb0?FLt5@ayGpuE7BW=VobXVgZ?;*mh)S2r}3scEUzgXX`XpOMNvt z-u$#7zC6)pE+d0-sT>LzGVyhG)=G$EPcPtEWvhy6Tgq!GC2i}aaL$3jEUxu(W@}s- z)aFtKj5Jr46hGMVbbONfil(Pn*O4mryQRn^%u%{zH{&3;4qahoTy=0DOp7|ltwpS8 z-zZy={(&s+jdE&T0wK-rYE~xYVns^Q2<^qx4C7t0Bax^p8}Pxz<#U&*eMTtY>h3Gx zj-^~n`$E$ub)S{>?o3X0?1EWegeH?N5YnM#-D$D`vuCv`zqJE4wfi>Ax{pV^DVuM!MD{{ldX{ztTIQXj!GvCmcEnDVbltxWlW(1(##9$=Wwgf@kg{ebLy2?)ceBJ03mH z;6FW$ztj;YInaq#rJoi4SEcVJ(+d=~hgZR<3Fo#3_m{|b(_xJaRq#0wkQ$mQ#k)K1 zWO?H=$vLluWoPg1km>w|4Z^~&c-AsMVRmN7zNwyLrcJ#oiPvvi!Rvy|%V9#ksX|VScH!bII);vp-2}i_8C9@p4wg ze<`9Ya3-=+h##>lhxdR|_XEqlPkKKPz5E;&P)9O%#|qXP>(#B_RPkg8d3V9FNcAA+ zB75;c{evp#ekDNJs?nbzN4;O%t>2}|kW(X1<2nxdNqk%XauK9|E)m!9iHAi7fnC7` zWR@o{JlI`;-#@BrB?(w01~>9^elO`k{4L{iinOs-q#qsX%ORph9(b1})Kc$^4mR(X z>VbF~+D+Zzl3lpACXI z`|H<+M^QLZ24buc{OGhcu0HrEY;Tug7#=|qtmkH}3SX7M&M?KBo~IxNbp)#h+x809 zvFP1guz9*iq)9y&=rSi6KN}Wtx8*OUhyc%>z$Ik9PpBMG^&gEwcdg#t6v?-&57pKT z=x+^5$gCc%4k{V?? z6&W;eu`&(=UI0=K+|%5r;Ock;1~qEm74QNG(jjaJ5Lkx{_kxzT0lE@XvAP>vslTRi zt^P{=lEzX!;G^O|eJ#|J=V>u_FaDnW`VMW|5ZGCEBmL#li!=T?l17~?#t{&Z4tQ4C zVbTxZK3;qLf8L8)!Hnc#&}=kc(3o~#ic}M{4Z?mG)JjcU$k>L4PEWiI-aPj3?A{EN z^}Rmw8u3aNw3xZM`DLRGrI$}-o!Q+i4b?TaL1~+sr)yZC4CBEL)oPmn`tvCz-(ib= z>+Da=&JN#ellby;gNR*rkFyKx_}apmnoR4Z;J&YRZI*9myd$5Oa>#z!L+&wvX_T-F z3+;yKUa&#ym|8$JEtxs`n8wtYZ;ysPa7zt(C?&$)nR`4sDee*iq1)>TIe`AeeupTE z2`H;8iQi@aJ6bK%==+LrbHG8dt1>pS7M?RUKTT8FY@cW=HPqihRDz0HnHnp`4#W#o z8chg4i475^HLtOQ^ng3(GLJ;1HY$VNK=rKJ#S!vJz-x-5D=#mE(bN`Bl%Ur=!+=Y) zzut_4dq12rMb_=R&e2@NS>>C2djPb6{Ca}DG4F?(A)h1i^*wk!XB_6+auNLtPD0)j zkcM9T-KTBm+i;QnQN9&ZcSwlJzU~69OIh2RN=moBkeougM1rMyRrR4``CR{puLeK_ z5CGylLl^NmeR{L~T79R6H$(G7HHSOxBZjqlfhms3^71H!{Q3Sa72F)p6x&oq9RatT z`N1A#sy*qBDhYNP5eO*}2yq-K4u0?-RxrqUpeP_h9}IBn2cA_AD&aqp#A>sC{Hd&b z&}w)7W75;}RgCi3)f@@sOk!U8&5?&g;B}JMEfg7Feqd zQ%7s1v!tv>mxrKb6wavSZBiUZmPgcD<73hpWW%&tE!9X=hT&v{qOn~?eXL=U9O^x> zu;EI1LX(}hr*0k$KLevXhxxi$@?POHA#pDdDB2#nz?n1pSWOK+5y#5&*}uAp#D6bicGpMcA+WOagQ< z7nK!1=2I7RgO6B4r-`8XEfLD>vkro;kvcvqDw~AIm;^|3-KDU_AxHzwkm-k;OO2_} zi8N)&xx0U8!ov70lfQ3rqm(LtQRi={b%-t~%(zS5y~8mtbRxBJbLpYLnWEoBY%waO z&$tW7$#g%hI!^3VlL>6}j;z=jVeOny8Q0Xb`93tEd%`2@@JzLG#<+TdRab0i475S} z({&N;sx@aw)#>5({%gLV++s}r zzF*gmx#pWZ5SH3eQ?Jyn8W!PCN82<8M%rxJV!-yyv!7ZFMdn`%N}q-2)*Ojf5+#ZZ zEY@cxoL`J9Y-6>-@FfYU10)QpGtkaVg~9Me%@vu@?!tICAk9!#9#P(Zy5Ai-?2tK< z=a?LpYm;oAcFvk*q8cTnDtX@)Z6KVfJ8RV-5trd-b_*DAYf%xy%qNww(V7nJ8cvw_ zT7%MB+6+Xov1}Vhq!ne2tsuT;+(zIGiCS9%lq1!H&uO3l{q6Pbf^0#sKo8-YIaq2l|TiBQYRYsTpI8W1ha$i92A1wnfz=Ey4p4YDGBFT#%V zP=^iDd*29gEHZiuw9}_ITd#F?@;rwVjlL8sm`zQkscE~*8ky{{^FOtYHz*xwuoP^y z9BZ_;*{HQnXaTU{Y{3smQ4pRsPu>)MYL-1KJv-cPp+<>xGJ6RiNMiHl)72^V&@mw) z-l5^CRTWK0rGGSA;OyyCns^*%6mcSItSI>><=K#>chz!sDMWGWg-#}ZKv7FO-3Qd4 zLBPdTCuIn^T=YW5Cx2VMP`)5%OD?GC#T@2^jDOCi9P(H}-ll9o^NU%7*h%7#VEKR=iHVLW*S&?!=fxJ`o*TH(X06Fz)SJ(($2@*+PCjys23UBr~kCO5PhlnT(orR zCorSR4e|Xs+%Xf<*J5gUTerK)c6i+>GI74r`1{XiWKev@V~M;BoF8QUlZ`8ZVr6`>H8EkQ-%xKIWQ7|p%8Xqy)+ z(MAqcJfW!@8HKq@^kZ5yQ%dlK*^X%E=kh04W-p_Bbp1GO34uWaXBtfs;RviIOAU`*ozs&C8GEp_tC?Bl@9w3y9<#nO`}$ZNwKykdAEE9TF#Jkk-Toa1XB8-) zR$g6E%qDWP&>3!-_4C5Npj?KT##UG>C}XLyEO<0ovaCz16)X%3cpk3))|V0$BBk&J zQJ52);Q{43og}W1L~&&KxHoWMhN6RT1e5~-?M7w#TuaTkNF0#R8KB>4o`0L?TwmYW zr6oXwlwIBDQ&iC^%kk3kRIH5mjw0670SZiQmQFEZv`1bnLxdD9#+l;IhLvxh>COzZj zMlOL!K;fpZjPSQ?dqz>_YfK*z#K|*|gTLL+3YJ;e5 z$gqrr%l%pU>+2oX8l&@b0KA|BH&dg|6PCCY>##FzV%^FPrM!&PPq?0NszbC!HSxJ} zhu5O0*$GQnrzt>KuilgQ{8FCs?4Ba*p6giTZOhE=0yK(JkOkF%ac0k>!VlB}(~}ZT z|EH?*L0CmI5wHQzYBp_Q?Aa(Y8*v4!CmVMYfQ8 zi>ZXzrhfVq;L&8h7&QyVjy^wHctQD=_!}=6u#9?AJ(5@Td}8GU7vG<9%2wq}J*U0iNI<*4qQ1()%wQSnv#?8hx`g$+ z_?@=ZdwB|= z^`|{d!n@jAiVNDwBp`DJ7xLAsGoLSq@K!ZjVZKhS5SfRoE31G@DaCDYUZm4@zb?7h z?#bg6gvhFM4|$JEN&DEB-#10asPl$I-Ji_%UAbyAr=<_~-_Jz75b8)|#fdeecb8KT z`dJ%zNHXe@d|k)RA6N}`LEmV`byY@B77|vVTt4rd#BpQbn-P;` zYt!jt=6xM0B9b0r^WHYBUo)+ZwikyP=tI|AY&~BWM^v6US?hgOvYpDDD#p{6Ix34K zffiA6=oq6%PjppH(EQ4Pv(+#9W-C{_^-eJ+=a2Lf4Oi%9B6QULjdb|lRhD+eqCBBv z7d%##aBblrhdi4^mloWCp%1{M#GF_{#QZSy{&G5kZx8r2&#I2IeBQm0-ZaN$J4;d_ zn2_|n&g+%uG-nz1%9v_9N7`;tqOD90Z=E=1^|3h6%4oh3hA$sOe_&_-1HqNm4@=-5 zQ=79sv=!xg`3k&+-qk04y?0{67Uk2!MfZ1y(o?@IY$ z!nvqZ;)X$ahxB$ozB=2!UlEM)^$qHm4}&Hu;CY`7J-ZxJh+2$HJ_gHgsm@TupB@}w zU|=h^QgQKgU)30P)Hyu028}GQ&Mv3BBic|Y@uBQ2@2-O6b19Tc@Qhk5|K$cb_VU1E z-0{`GTZZxop7SO3`Dcz+5#lLIZ1~a>_0+5FH6~{80>PkKht{&peR|Hbhj2?d+7=9KjH0 zLZ!eRtifdz#XGZlL-IUU^@f0Bswc}f+H%N$=GP}RZb1BS6050#$~2?S5X3a`#Y)EI zp|Z%#lKMO))BzcZsg1UiW&Nts0T#8#-&-PC#wYfSU=1!SQ$S!fg{0b?(g^2S9p%Cm zQvd9KJqbquodD9W;UU$>!1#}9z5^}L~A9kw8FZ@5R!ybHZDx{o>Yy!rScO;5`?`^kd6E^ zfBe*F-r_ktd_ksJ4^-Dn520f%~HMKJk*Y2f=Xf;KgiO zV_#aO0iiq-wJ5$s?(G#j?9Y=U)BJoW*Y0zxCogzsYyOAl1l_3?{EOO_7BNWcMtZ)}3UhsEnb#iaFxktF2NSy+) zi0@GN0|Fi2oKyX*^?ToBDJrKIOAlz^+!*xp=jQ|ww_BFjOT%@jx}{c^n%$b!-d4*% z8uea(Hpj!#nwp3BY>t8cES_~jX@aZ^Nx5^)N+-To>1+U;p6_&!@MK-oC0OtLm3el_vhk7CmG5hpXc3h{VYDogTz)syU8Lm1S~b24d=-5%al5 zwE)$z4nH6ym4tV6mR>uQe31YXm$1M$gs50>U|xbx!B|O(E%ju2XK;_bH#oENME#kl zq+rZn!3JDM%3g7SM8^-e`^;A^Te*)rKOuHyk3kbk4${B$eW6EmCc&!Dbyd1O&+46B z(M_3k{*=P`iem&_k);?C6W#*@;o}8$xmgJ(gXJ~<<)t8&{Cj+RmXBd_bN{bJG)O+P zGcW&vnkzIsN%4n-$}d4>^)L`ALp&_T{pnNLHdVp1pZT^4MDqMR-RrT2XjL zS>ZafC7Cu%kEDK~qbHIc<)h<~#X61H*mp9jOh6@#IOf*zW{6Xt~Ll!ot*b-oxf*E4s1Eg_z>@yL-+Mw~!@&Uiv;BW2frkGX6V zKR(#+5bv1q^b2`MR#(eb^A|{fppHN5Vvl7o|K=o(45y|WH$teoNxC`StZP9 zaChDf(S|_pa?SC~-Hp(F9Cj%rm*`g2-T`ca-+g}Tb~HOWKa>$iuU*@3#D=g@v-#ox z_YA@wYaTl-HN-mrK*lZQYpd5Aq97B)p@lmTwlPvUqSh>s)D{;4B^xQ~>)je^n;?G$ z3j{;R-v1JO^RP~s8aClw6zSnpeGGl*c{|EBoKT^dwR6EX_lD0j9%9J&{=KL7a9$q=N>deI`^fIRAV1(AxalK z&RbpiPEO&0NR8B^vmZj1Q+6ut>=rQ{+!roaogVbovESGM?wGf_6*u(K%CHppvc`-U z8#2P=lsOd?geDXpPmjBeCT8y6E5HF?^C*J`-08xWgm8Tu!zujUOJun6!aurI4?d)o zY*ZSnmj#wkVswixOJ~#U(c-7HLxEtYp`xIOw|1j4^5=BLfIQy?X_wJ1A%mxM`Fqm3mD?Q^ z>FvovW_1JYLP#s0?2)nIF;?mBi_ay2R$vTB^-odgHv&z)oCCA;=CEM9Wu6!jy|W6( z8<4EjdvXlOlK9gM;TKG$_+8-%jJmIJSyt6QSK9|`p>2&B+6Qy`WNmMz zA1Ofv+4AHSc|fqUP;9X(@#{Nk;v5}|&z<`O>Xq!oWXTsNFFI5<(N5xH_jqxC6P zzJDqSoXmLgQ@3dJi8LW2QVmi7J1GA7)ZFt*O(EitcHF`dLjQVTV>k|28xhlfihR5H|&bHp!&itC=(oore$m(fsa0M1m7c7a^5-Q5#f)?Zf zb@t9AF|65s!J*O7_O2cDv&*OS{sqcpw79a0#Hb;$QV-}T9=xR2+BI8TY;g?j+?moC z2IlLj`rWmn5y^Y+1UK_M_ZEI5R>YOZk7|WTL%5mUa&q2%A!r%>&VK#wmmDTeYh!^^ z;Sg(Gcv^UaWfK<7ie`bGZ3>xE-)-ubrf1hSGX3MOwD&R{`Xg?u$_3%&kCt~{M1xHF zQi>XZ;eHtE}F|A`eV!O zW6Qc@Y95f!+#h}!S&L#9pzj5asKFxZyWXHM#diFKiYvbb+rZ7LE+lj1K;O!Rd|CLc z-uK7YJ+7=ia3At{NK|+j@nAnfinwn*T~CfAl`ofErVH-cWb94usGL$yM!DBKZ7?;o*(vv(IbEwe8)DMyK}uPqI$U zoVv_ml4zabuwXrIchDo5c=ldOSP;V7a!lX-Ug4Z0R>gU-9d{wPwzJZIlz*OV``~ev z1OHslksI)3g5azC`c>JYMRoKDtysk70)3Fg|4+M>|9V!=uCyZ~gt#9i98c9%PqYiK zKH(fI0M0gTUJ`!j?8}llmnIBCNb6y-`~r4cbE$ONpgq)h%thQP-r>z^3BZk_cd6~#W90z9me9>%znFVuwo##In)6s~=gzOR>KS#3g++aQ zrdk7_i}sCgQEtp#V!jw7(x(Chx1BvnOCKT43|+(BTMRRt21|dId%rK>B?GPvEyl z9{E`6^eMVfZwG98G;H?Kfb`P}q2O2^4_ij2)CV-OA?yj^BldxAge}93p~Ue41}g}$ z$k;he40ls18^@-zKG!)H2DmA7zs&v@5d%DtU>uBqC!HMllT)*kMW=eX9*>TXUe8l| zG(?tuJNvd;>hTal{W{PVZ*1K;r>d;HL#=f_AAE~Mi9K9FQ^c8M=C(_W)U}enIj=#x zyh`mkqpV_ixOI(bHgED`x+d_Z>v>>1KG6(#R+}f_6SZJDKI}xIU1V=U6OiBVipY?Q z-cJ19Z{2^s@Ao{FyP*UcQdbB{$0xSMwU!C^M6V3hJX@}JhgeLy`h~q6@tLURB89E5 zd0E@&Gezz`>EnTR$7hK5g1*@ew^YBcgP&bJ%of?)=>BQT}QDG8_J^Ben8 z6@@FLo94>1F|&usAWNq~ZIS=Q?-kaH?uahP5fv6lpDzsTB`|lRb53xlj`2s(kkEiO zPuCtusGyJN10l6*4h%^7ekR6(>JRd7l%92N-9Tm^5DCb~kC7lfRlhCab*$;*TR5;1 z!RIY&TLO>XDN*kJleZL+cl;=9PBt`zf^}b26A?ZcbSV}VSj)?QO)SlMa)7I01`zx` z_XG!Wd&_+T+vKp10~1do;cXX#ZbxUHNXrb2#~tH$6U8Jrk3kUilh5O;7?nL0nVy20F1w)ik>=dYLlxCC znRWP7=x%IwZZ8&^+J^@L5(V!?rDMqnd9|23gB@7WVv zEoUGYwW?_fJaVV>O!wQFKvIxTpRW(Uw<0J=-r?uDCn%8HTk6|a90cMO7FwO{?FaJ@ zhaj{u|1_$-+$`+C{I%dti?k8mjEP*O@Cm@P3VbDS2f?NN{JB>JQ-)v#tz#v{ZVt_K z&!X%a_=qryl~X$QtB(J13j78FiMjS13h^M6CHW_n3_dW@Zn1COWZl$d>$)dI{6-$) zH8DQ$$E<$051qX*W52EX#mmr?d!_j7t@WH@8j?O(Tt@Fjq8L3TrSu`7eC~O2dC`q> zO0zdNa*B2uD8XY0gP=y@C(E0}__dU8-;Yw_6ITrjmA(rKC;Ch#*Kcw{q5lGyY$c!v z#8AHiXnof)YPyayj{460!#;@E_|tHOH~J}7xr9;QXAJa7Z}yDeewOTBWLl&+ayn{} zdzAg_+dbo(HTeJntBvww*Z4O{=0&;$d=(1R6}2Emx(XQ_>8-vQlPf3rTqqa6G%8=^ z3e;?NkKcZt5p1XceC%ux5>@%-ZJdUIr37UoG3{J>2xDhqrWe{-rJjneyf(#zQhj_o z{bTIIK4vx!8h!mF&Z(!-=_>Q?ubt=|8uf`yzgWnWav*{3BH0Iua@&N@a6Ll0*iQWY zgmVr>t>B4@3|s=$1GcIB*8Ur3?g&H!aQaK|{ofAs{_D78?843$1q{YQ@=c&o*rUpi z5@Fctd616Hsv#^$kB{KloKT5jZ=sl&j7ySvCFXXoQM%h6mOHaBFZ1Ztpv?M9k(T4- zRhC&T{4W=?*dp0--`l*Wk(eq}Y`D8nL`c7?iDU&RALpnF_R+f9^qG?&=HNu^pabzYqGcdh4k?la}7LiUm z-Fwuj5$cI>=)MkTAO-sJx6$+9vUK55*!Vp8qC=^GHQeWr?V{t1(%2vRV6N`bk|yIC&amO9P2J;wEMKaqZ*SmI~fg!%t)OuN8Ci(9GVW@#CAd zlwJ8dMuqpE#2v3qklM5wEKY>wAfkh4?ffm+(A1_uOu&9QE`DtyJ4+j|ZWHusrd+a+ zVeRD4zvOy+%NqZ8VsmIc*6siKE|v&$>pz|qe?y~mm8i^+ZL%Zf^$rf<&=`+&A*#$P zD3h8J4z63)!z>1CEb1T0Rz13y^isBF@24JS`5ON~x+VgxV*1X))=kO<4kY3)*f?Pd z>N0ZbvHFg%@t8!^?0M%lVn$9o!QsNNS(NFVg(JLzjL7Mx7&#-S6Qc7+^&MJE5IKQD zXS2(8@%!Lc^V8lhKnc&)T)6vsF6@0&K_{@74>WByo#6e>?8*Z?=LdlRxoK8lmfX?R zAz1yI)xiQHkD*+}CwG|@Okl=2Az*)}t8bZ!6jJGsVBouQ)UV6iyUWjC)ESk2vQBT> z-w%K|AnJ$uAU+X(EWdh}+Zr&+f{ICm0v|%r4~OH(AinQqRu1uyMp-p!^isD`+@C~F0XVP#^2Z7 z9VuHbavrvqGJ^wo><+VqXm@k80GcG~>&beY;-pPCic?)bJ zdOKPwC><0CR|3<)k?xLepx5fq-3?CX@_-7RUZ=-_A4(nW?C4yjo@X&(c`R0bK9iLP zV>0vp{K8oIC=)jLPNAT4aeJZafux507?lHw4NP&0Ikyk^dGt0~Fc=;jkD&ZOaqoryUj)P}>qpFb2^C96R60~Ac-3TrT zg-c3?Ghr7Ba;rFUO2uqJr&po4WY(VzFOy}G$$8nHyPGWN%xWwXul`cSguw&OKy3iA zDy7Bh2*{ZQLH(;!(@(W(((+H_2kX;QOwiC%eJhoL>Uk&fKB>N4>1a{8G}lcn*zsqZ zBAc6_CQSGeLxHV7vTLC!wFJr%wfqu(jHo631SA<4|4k^keBZfPd_;7D(~Nj1kl(yC z&*Z@cZtx9zH0s`+7v!J!JPByo8}I}U7l@Gb@TyzXe;XO6&3iL5^JaE716R+ky0r!r z>6gR7r=4d!n^woN;`8WmST;MM_>q@rqMSV=qddY6G?5$kZm4{JEmLamUIqaB+I!14 zdN}8!K>9@BM>#nR4D2gsy^va576SQE z=u)qA=ob!67-0Ivy0631uO%t0P805xpa=YxC{@2q(XUh~2Kob3%09V#An?fh`}`HVS+-(kuPkA)i#K^@ zaWS}yt36tut1%fz(gaH~h2zE378?07#1az`F_s7^dk~tI>)vEV6UyZ2)6S(Rln1rR z?sd?tbAmOhWeY9>amHx47713~$a|;QbQOh(es?m#@1L_3y^BG05czx^zRH7@?uuD| zbw_(DQI^1!7qZk%3%BwlMDcOL#CT#{A_1^$Z~oCDI}qS)g3)+o9bE;hTE0}Cky0p1 z1#kms*@}f;EiVzN9~9wIpX_C~TmxtdxIM|_!XBCZ{ZCO`QR)YKh!){OHaMX@dzEpr zb4-1c+-$Y0%D9~TOd2J7ZRCVD-!&^o^QahJiQ4^TDs^r^*Y6(*C` zBBJGH(y7_;kyH8t#{sb>&>J465^C=0*kEXeKJ%9)^6>6v*0zb?s(4X+91?G!rU1b& z!YQEm0uFWOsqwDoJ9c=L2u;jb5LIh?-t4LSmS1+>e?6O}i|6pqGiJV2V&E zGmwnuH&a4SAy^~8ZP73YP&&WS01wV7}wz38f|-^Qo^QX}17^6~wG zj;V0J4ACQv(8DKvg*v_01;3eVrzudmD8NF@JYBk{9!}WpK6JaFMOzmv!Ae-@h_%iH?VzC@v61*RmBU2 z=g0g4EHDNJ?CI{JR=GkuAoMH=;0+az4wEoW#&}}zgOSmcNtuJa6C#me_wNe|n@!7$ zanK8i`B<5L#Tkn*3ytjw)FdEe;8mMxh-F?KX|n}kyq-J*w-L_`*+&jG4`IBeCLUjE z6!GPSLcZJxc+=ezm-{LzX8Y*Kwv@Hvq1X#-1Wu$eNW>bGh{#0`4=S#ej5`6>kk@x` zv?-YzYO>%Ins{5nGz!xW*q1xUQsNr~G9zCoD-?(%CLT{}0=#Qk?N>S~2{XMdAp>N2 z$h2b#WFE^ABRL5x5*4=)homU4u2@E8veYzyw@3ek&2QRnZnCJ;X)Di3R$u$d5rj(} zuNnUd6u&_;+#<0ZNh=+dr~(ZP>0Og*K&yC7NKS**&J6`EI64apvec);SamhOszM-B z3Ix(Zp-5`t@?>hj#=V~WF>Qu)(aDgy+Wp3kL&-2chD4F$0LC6|o3)89B-X~p)+Q#c z7X)C=yz9n8dxce->(*KDqK{Qmz-nAcX|qQ}_?E9piU?i2{rJ!55x~Y(1`?~N-maZEl$%Y>D?l*{vb@tZ8KitTk^|57$TW7gvH4~4*b1Iv z&*6=5Ph9FRFBc=7_I@i;kvUJw>$YlpSD+ zk7qkd^FyfUk3KY>NImJDu>fcA$Xh|x)#w0Mx=-;Un@!H=W62a5F3*0*HnyNl9`EJ= zFiTR<67gqBEVbnQkM+w3Y$>*6>zCsJu_5$!{E+*P6@`@LR>I~dQdax_^Un9j;mMS6 zOF^?^DQ|3lRPDz1$NX)S4ZF}S8+Ik;FF;11pPTMzjd{d&Tmc9r^J=mA)FYjj7e}HC z{AvFth!|M_!ukvlJW_;zY;CG+(wP!Vd1>Fh^T&Krh?eZj-HN#L(ELhUGAk1jhmXbkE5fyNlPZ?+y-L+gn;(+8rzpF@~COQSBOysiUbdsx3ZXRx)X6 z_gt{wlCIgycz-^m13{xBX@>{+cagPsasYdJz3KaGWq63$%5Lk?0vI=RwIJNqn9NMi zQ=v%|DLQYu{7eh6hFut&mLM;2lyLg(VZ)_>kM7a$El`{1S@&+`nG0OJ5lKSJGov@ncll=GMI-Vja=W0~(_0p9|=`gT5d+yJ#Glg8Cy z^0SL+265T<13Z0YsO*Smc#fW;Xr{(KfdH@rKd#~Av*THt_E?>RMuS1ywg)m?4z<`$?KcRv`z4D&(-3h6qyB0Oy7t zYv4I7Z78QT1HLtQ+%!^S!cr^BB;kB!BB4=uv{<_brdIEUNpvW@hFl__op5#v9ji4> zAbt7${VUB9WBH42Tj7cr7bK$18-RdS*Q{&_+v_KWqnY zCGbsmrtl>76ON7rp`0`i-I2D%<@X)UPNl+Hip;%BphMOH zaPDet_%>Xavjb6ji&i`2Pe>4qt);HsT%m2$)X+9>thBDx;+*KanoAtIm`r+iOR+-- z2*cL0Hm_f2ZP(VacCKAxZq=e3SzSBRB1s>!9Ld&KEMpjh)f-9zyb)<>`z8hEDqCr} z02Qoa%_*o~c*UFl&Q2|GBXO0A8C(c~(zBsif9f@qjQX73-3`GB(Oj8Ztg*P3*%X0D zY%a^Z*q3hRMrXh|Fh~xC$P(5F^NHx3w8qRGl7y^5Bu`P7%@mbm5Qcm*RrRr;w}3}9 zyKuGvyuqgTXqJUC$J{rj7YI|4TZTv}1Z?^E6``P_MC+G-PovT=<7{Zli5hSrw~mm0 zOFbf3=pXdY$HpyR4i<{|pTPp{_W_xV8pKE0-Lfqz3LF?9!m!$6@$ zLPD53u=!w~EU3CsWWD)J*aI7xg!0|}FgDZML;;al9j`5gT+DD?wf~jsKw0^4otSp- zm*#re42{pX+%7Rdb`nT#PUXa8Df)^~gh6sd*a9)fY7y&z8$XQNnTv)tV{|kBj`v(! zQ_JhCT$RIJu-XDV1%<>`Yh$sw~2+N_NNR}KTJSz(a%ZFiMSy^~E@D@mK zs23|7`_RlFhpw&hEwhS2pve_Pl7}r|*YC8K%}2&Z8d&(@##;ZYlt57rPN2*o3bjNm zUyMevM1aVp`e0vY9vae&75yWfbzWD+PE5d1wZG zJ2u#@i6hJ<-Sqpf<`aG>&|cMkery7d6Xfe{xz|DQR-yeREMu{Q`qq>|XvPnE=kXkh ztGFFR3_Huj+o93=xeIFnz9tq}iqLL@5AM>bZJK&MzLn4B6I$v$)Y>{ewq*ku84y2( zO7LC*KCb$Th$~I&`hLO9#6+11Y&^okW?(INHUVwU$ML2Ih22jkXBog&k}@HH@0eXH zG<+eF=j)$`#;3x%4&~K-Mto^%UT@-9$~TmhDKO<_KpK|PvnFKr$RDm%XjA|-tfF2}io<`C&xnKM?7^6GxB_FzbFwJ>|1HR%UYR1AGWIFrWm+9A z{Ct}_IaMD9>ObeQv@Yem(!q1IoIqX71mt2+a7$3zX*@JS6D%q2#3`;M9v!(ML zGd=yueRK1pWQPm*G%*uSCZ|c-YP`dRKC-#_ll$<5)G@%622m1J=6FdAkMws}#GwEz zdx5pSsLQZT;X_VMK!8%YoB5xD!ZtlL0GQby0NCF92;@Mapq6tBfw1)S?gyD>U>}Fn zHE~%0pJ5Akdtim+O;6oIWEJ4S^f5mYl#pGK8T;f`pbq^#0M>cmAPEVME6@b@W1+sD zsp0=pG)VA{x0&d}^^A-UHr_%%3I>Vp`D+0C*2N=uFoHpEucYV#495^H*aty^=YeTv z{m=sRdTNpd?k`>Dh)-XwJ%hXC@cGJsXUkYqG2U44f_H3zR`J%ZkH|k8Yy}y}Sh3(0 z4?4|IKewcHr;@pY28qGVq4LqzYQl6#^4Q~5-tN8&4*SK@;F3&9lRt9*2EKq{w+Dgq zX&Ax~6vFTDGx>vHbxv`9A_z?PeUJmrg@^HKZ-CAwbPacDUe05Gf~V&$e}Ca8c={>NV0Ikv*+i^k zAk?iUg;$fbOg6N{WQ9AX-ORvO7ITt%1$~qNn$sw}p4R3!h_upwmV*Xnrpz)CON-Le zLhtYJrc*Dtu=8biPQm%yvL|mF`uuJtFbB}9!KXl||I6Yr$HdJ+g7%j$!QxABFUwsH zUcSC^kNH!lSW9--`6=rLlxe^<^ha?}(r~x2oE?s`I_(s!i#Bt6T|rme#of&ekl`;c zE!;E3JUOdCi}PhqPv4^2VG8_M9X+Z&^4Om&R*$9U(p+}Hzq5B>vvkg^9=v$NiHCr2 zfd*=N1mZ-o2I>p&QJ;f2WnguLn7!S9tm1`ScRnoQlx=?i#~w$9eE9(}Y)sxOmG;UN z(%n%hSM*4g3c0kGE$?AiEp(<(3}8&U;J(EdHH^lB0^W90p#!pxj6@Y8NT4udyZ=JR z)b*(g35dJGLu#&L!t^A|>nhlAmwhXEyM$Xf^^u!?c;oL6|MB8$Umu11JL8uZ{l$eE zokk}4`qE3|0I5QX`0?U9KfmudGO`BZ2U7<=N+ra0#m#+qcez+9BCGbs+-52FCHA?w z+#rF$LB4Y6IaalO@-Fs1mZseP6dM*%#)VDAuG5?{3N$;l_&;soHWec~d>c47p3@)Q^0x;6N6$}JGboN=2O!JQmD36vCl0Kwa9 zm#?3&li=gKU!mQX;N{in=PT?Op%>`%_UZ6tDlmP$x3r&BAOh<|yAlqV)V~vj{cSt7 zomdilb`iaO_gI6lP~vpEWvG@!ltk1$4$FXs6hSe;%Jm#*XM&1OLl&Ayro?RMJSLnW z6Lp#*<;+Qcg}Ge7z95@gCW6r07YE=Z{m1Ctx>{+iz|G=VTuU07npfp;tY5zo7Gf-# zA>{E>&K#={Lz@=UTzy090rLtnpJ^Loj)4j0qhz|md;oG@i!p(zvKF&JIQ`bGG?9jS zp>N$}h|)yTZ{JK8sb^;SKW=Vb^@n>N`!_WKKuPso-~9k1MaV+#mg|QvQd7S<1iGKU zu!q=ePcr|X?3o+!JXI`sYRmt8XsHqcl!mJ87GG67_sq&41X;k7{SX*RsTBIyQvSOR z{)?=Me&qjalK(w8lPR391(2V2m`zm*E6pIL+}x?-uAXOZF{5Qqf313yE64-lT2 z@L?U_H|h7WGt{H z8ALJ}p6f}2w~P?Of5pwVqu-Oh_ek{wdctSFj{+}kcBI4z;Llm{l3o@JKyexE<;yZU z2o44sh(#+40-ywVZl!^sAOLTn@xSQ2nUa&WzptNDdiRzary!7x|Ikww3S=M30_<|Yv4h;Zt?r!!wN&l!;`F=uxAHh4#IKgr*tkc@JsBTZ~+*=GvEf;=>|L;=f>f0|*H&G>(rz#>$xtFWMg zI0IWM;*v8RWy3c}yNU;Z$lB;;M;d(m70SAb`hjfv*+kr5HYYBSMy%}R>zCk@SlCt_ zT^G1&wmLpvYEmE+O#Jnk{`MoNm;b6vzcxq1NfA>BEeWLZt(F#!Rf zXIod*esHMO;(Ub@d;p=hymv_8jbPVnL4m&>@1`R3zjdj^jd_VYY>GDczPZJTL_28r z<0+V~b9hK6r*NIzvl*a!C^_3%?`Ar$C|;mcqf1%%^|yvp!d_bLV?sgr-Qmm}>oKq7KHdoM z$gT02)x}2oO?+Wx1-_VujNS&1G3=KSRiuM()J#c?>sdQjL2l4R(m>hozrFsjdurW> z+SYB|uf`xMs6)6PB?uy|p8yQ(9)M{ThP)P0tJ9_zWjZP^Y!&X+8J}R@Ky27JAViX_ zvwHG>P^8uZs}1k1QtQt7zP~BN0hlC48hw5>2X6b&J3wbo*SmkJlt|QkOe))L`@Z>| z+0#HKbu}$=GOD2ZOTrISJU3Vos$$(ujKjVB_6r@t@dH<%uo_W7reDSyskZfC3F~OYhKF(iEmfkQNF0lBT$9QC1 zjbj^NU?SM746RQen;tv;@H2iry*(0*x*QAa2U=GFx{29WP4%f)HH&*+oaN3X^H|&e zgdjUVBvx+IfvRPbYT)h@{#w08X;f@rK)Ko8aZ~yEB_n|vL3s(M^Xf?}KO&oC8-GNS zZ`9={oDMo4kdic-pKqOFdnYes1WWsyRC>8~&xx!PvUOvnWYSn+oowBBA%!?z3ifa4 zrFe=UZGn}3I!t&%X^Ahu6ScwxOFqAlfM)GhC;lH@S>W-CS)V6&dUpl*$Vm>%vWMR( zR;|t}wygpYiuxrM{KR-=>{j9C>%ULo17R5x7nY+hP%p@q9(=8zQasu9;q0af2*`Il z1otCUH&qpfLz8CS&F3ekmP%n^*^Vhj-yjUhXaF#Lqn89jlrRrz8kH#u7w}{ zDtpq%AvNGGBU_>Wr+#fEdD9W!sU=xyQMVH&ptwPEpjVrrKR|_23#trJ=u9m z(=yAu!5TryU$-jJBbmqS%Ec6rb7OFsL>v63(fQ?HXZ^@_V%FKy4O=_!Plw0JZ7YY~ zku?E^5}`X&c{~<5L*SDOQ5n;lm9bxAUX$bgAFN`a=VOyja6%V(g7aGvlRXYdci`&) z?QVDqFebroQOEnO%(=8>)Z!KK4oPO(@1csi`rY#MU)hq`);mju#YZ^?guPwfX|r}) zXn965wO}wO#-Q_>;Dc$B8O2AewS){~>O(KL0B#Af^3|O1eES}2x7Mafp2pQc&NpZ7 zQT7N3j&3zt?#C%?G4%+kuq`dVzDkfJ1}gqL;Sh?>YNe7g;p3!Sd-|3%U6BZt&5O&C z>v+?`>*p_^nJFr-oD^PcifZWnXJF(`rvg5u>Cm9z#=ua`aA5FoDBuC^RyXm0 zOo2!o>RvEcu+t6)2aWifUiM@2*w7}Rp2`LeGAlJle? zPC?jP9;ffTEyJH+XFL@2H26v@^QD}*x&O4Nj^l~SwaDrgbQB@naWoJdqd}+aQg=pV z%~VoCTQ24aK1{cPWgTxWFRx7<70zR2;>iz$k^FP138$b4tleuU^mq+q^fhPv_hYRz zzbK{%uA}RSfp51K`qIIZqM8>_%u+#)`}*BX6pwJ!UTH21Aq*X@Mc~>z9GcyV#!HG( z1DS$JSNU>E`yWGqfD*(KDD*CsqSYjot(A_chOLjzS2l~aDP!wU&{#*&0{hzm(G%BW z$@9;LNT}uJTN?S1p*SK8f=M`uhS|dbD|>pH9m+f3qU+a?n7yGB?4276^lo1(q z>(B8lmF{cq7;x#d4S8hw%W{*A{?k~Ck;N~IO%|X*CByD5&FBo!4S;qbk{#-n6n?J- z?ojyTd}P|pR%P6mXkE1Depf9-7L>4&r;n|siVEfhDgBCEOM>2XU3zTXE`Q%GVm^^y@SXecRse14-_RHhkT~^ zQ zeDos~5j-h(`RQqkrfa3j1&rksRWNe9j^pytor>|r@Jyrl0rlnVwbAvF#>Om6Y3xJp zd|h}JRo(LchlbH64p%WiN(u7jV#R-ZkMy@S_Q~&@-?Jg$DEEch5(7`=ns@tcTr?rO zTn)T$IyRIUN-dB!-8g(nem%Bp4<_?4OcnV~txJ-x-d-i4wx7YPGv;h9k!a*(fxc^~ zzmr1jrs_}GFSQ2PH7|7~=HV6*5#^+U> z9&4#|WOM}>g=Y`KQ!N#hU3}05?%epxa|u}5R!#ixBqL(d|NT`B`HNBMGoa?A?B1Hz z#Oag=DU*t8+IzAO*0d%SQFl<|r#Cc%?W;nRL++Kx;zI7c^^z?Pxdl-cg{Xu)03JMZ zlqxxYC++q1RRDu_?$o?0ieSAz5cR&Y%Cnj@xG>Rq*>VMcwxP#nPrXj_dSGF^^NQID zzU&TTA8ny3>(H5By*9I_Ds8rIR^GD@{8>%&hF+FF2gD=Q-OpVFFZ)us?D(v>I!njq z_7>zsJGmXRG45j>U^j69Hy6l^9ZoY zQ)t}Q3WeJ4wDZ+)@GZ5}@m-6>HU(eJJ@nAwKYT|En$D`*iSrHPzYi@t=PQ*F6RLh@ zLup}Cpbb_@(`;ZVS1MMPW!60Iy(vAkbR@%@tYSUQ#`@a8oJpd!6Kaj8D;m;#fjT|M zzfV1=#As7H6|waxT!Ue8r%NE-+ z$7Y8nEvOGT9pbbZ6eWEc{?To+^%DdVX@XcUTRTOh5U0yQQd*8ABmQqQv7{#sZoJH& zlF%?xyhx1u`$4ih_HR>J@jycMXZQ9K5XXz>%VPgt5bKsq&KbyvG00t_Sew5^EGoDn zgWHlj=(LZK1YYL`v*HXw7dy`E-$%Vpfvm8iD0=`Hq>k~%BaRlC-&g`hAfsC_Be7xZ z2#Bjf&t9Z0v0lQ9SEW;T-tA9L?{3|pl-!}pIXqf`q$^vXX{x^Dn9`1H;3LLlMpdaS zoRJ=51D;hjjH6tIt_c{U2Hj8@C^Q1;f^6=z$7JDi@R&|}L9QEaH5E1W2;}y&{i6qM z8{DDlP(!CJc-Ld!KG-GJd$FnMBq8iju&u_ZXG`!!{XWn?)Tezz941Z^M~K&H*IP$g zr(1_xfe41WlJ%dgZpK_dM}MndL+N{x@GYQ+uVEbaND1DiUOc0j4FnkKFCYG7q04=9 z?A+=^=R`X;WJIYN)@am2Bf%Qtb4e~gVV-kWBwRL6!o^Tk-XFF_9uytzE5OM&>QhOH zu1s5^8Mb8&7t3zVt(w~S-#1SiC91Lwi2*qs((Urxvuhp^4y+2=O_L`DO57w>9`x;R z*|sXx)2QN+V?PY29dZ3~P82*XatX(^n2BdI$X zrzR?`wbz?zw^TKhP+M}qEL0Z}0VI9a-&T%Bnpf^5(0)&D#o7wchE}=9b;l`5S5Tp< zj>~L2No|tnfIfB_p`f5|29Z@8KUJ=f_SVU&Kr&*l&%os*vl+AerW{{WeNO)0gS^#h zb=#UNPo10%$})}G=-cMm9k7l$@$x>fU$A4I7ivcx-p~H9;@lt`I%lb za+1|+B)qvx%XP*x_y=Nl`!$bI2djRjb!{K3TbDi>a`rM5y?3Pbf1=;mLoQM787L+@tYFR}Ww5c^LmFe~bPZkK5)CB3Y4Mou#k* z=H#u9uMZ7dsPoNKgTs%nw~}X|#bi=ZHGT?TRZJokLr+o83Z4FA4ZbG#x+FcpSCgid z8+CI{Bl=CdSydP*ZywW?XZ6Pz=fkyYQHrj1rB8!Rcxnc0adGjPD7^$gG3(9@?p zuolO1%!c#aa8mlYRHs zL!v#CS!crH!zQwTy+wdFaa)`7y9dN;K`jXxb0qic$FEoPZ1xaQbH^gpXO9&^8l^gHjWZ3z z{L;`r68Xvbz_TBiY`WIyuZ2F+x{t30bG&c_GIAy0)63`4%Dw7s&zHU2q+Ae6d$qT+ zz-RPtarN(6+`Rdm#-=eU({gC%hsNzayihBTeY^v5`f6Yo$RkEyB(GL-j&b zw=2TBZ}4p9vDo2NT3J}4OCA}vEu%4tu*w>C@Ymwemm~2iNGreZn5?ZKy+*=NK)(j* z+J3h6@{p|#j^j=T{`oevd%j;mSMi4-uOhLl^|4=)yYhIRk@o@L27xqS-9yPU1J5g8 zx7WhD4q7B9h5xVXu?z<)<6hdnx@-DYiP}FI?R)!nqLJ~oVZl8%2S1y~yH$h>+p-h= zEz>~W&b!7Kkd(5lc`KwN*Ki`P)#Zfu=^t`-$wZ0$6U{$YFK%h=WaA}+W7n=~C@Ck< zPhNhNw81hLjGUNxCokX7FJmS>vB|hqnwrs-!GR<|`arnfB0t(g=SSh6HT#jc$9oUX zZK?tpOc(NcTh%5HoVxcvqC#okrAK{tW3XhZ6lZ4P?{Uw@^-BgPC?MNR9nSjM zP21<=S#_b6xnm?UcC31 z^*-L_whb0ro0;J6Ak>m%wyQG_2}e8T$Lkx$uMXur{i)5F8cR_Sk@jr;25i|i;AW+s z47NLUM|Mqr7S2GZrSpIK)VCwaca-O;%hgbZD%~J;`()%iVm&_CG1Dq^U#Jzz3Qnl{ zU_Scvq+Sa6oK%w;rDSoUR9Z8wmdTR`Mp*uD+qX()d?VFN&Px5Kt#Q_2@|6D3wo9vp zz{31RJ8(i@h~Gdt{$LDY_t#}s+MnTy`ud%6*smO^()h>MQ57kD{#R_)!SuU`u0HE* zR~0J8VlKf3y~3;I1(m*1(1L-S7`?u_2tELn@`YFC1OQcrK2x8$MU4BJq*@#IX{fwS zua*dxFu$C~yO>eT>uFR5^8)7Yx+Z35Q_lLb1~GrqKo=r6!NTrbM=MQvx9-K)0J(hOR_5)B-+ELi!Sc{gX1qnwh&7*6d1 zmIoghzeD`nOu}uUOWv|OEOp=AwRa&X%)Q{MT44L>?;b!S_jWL-qelTOee?)$(1RX= zXOju#6;t8#v~)btU3EB=l?uZX%AHf;%rqF8;B?KH37z;V62XZaib!K-!aMe~B&Wr0 zb``RTOoJKz9=Rc{d4lHrV85I^(gWr2c@9zw(zA#khwg@E(2^Sdfm&%S=iS&Tw5mgI zXK}>86r3XWkE@ZsM_P-4?L!RKinb^lnyu){`esWSRiCjlLYXK$94<^I^plbGi4l<3 zLud>I7lOdCsDg+6cwGdpX2wCGNuW#)Fp5R7C@v8P7I-UTzwwT$s)bW_|KhrW!)=rU z&(0|%WNUwO^kEjfnuBCMK&u~p(MqYO((uvGDM-5BLtkR4SSB34@J-g)5Cf@%#Sq*? z$BZ{nXm z00R5~nnyWW=q)CCKh9|2xaVk_$gQIh^R3#B+hqF`tJP|oMwE2QiE&KgcPB%{jVD6S zX@jHQ=r79b|>a^mu*38*YK6T{qj>?)&+@O*W*sH98iiKW7geItCTw&^FY6O zw&v1w2)M$~FDq>0&#z$Mh4b@fN(UaNEba|SjTrZK0$z(CC$E%>kGCPY-Bdch9POt6 z`ZqwI`VjrLtn4)*#_5c?wX^8KIgEX@eu^F*$yI6Unr;Py^!t^L@6?q2Q*IDC1q%*K zmu=`*Ajn2-d!X~Gxa}I-Lsth>wt>=B&pq|>J++~QZW}S1wF{jf#N+rZR_X z5~m!L+H@}*wKdxvTo_Q>Cir5@EGM)O&`%ql0-aO$wsYa}_wU1$a_?;e#rA8&YoK1M z5KTm58?a#!3&CX+g-)jT3-WN}0ZBoi*rrfgTUztHq_zYkHPVr{MieFqt_1EV{%r{Q zTc36@?LYe$lR&EC2Th{W%5%OTpm@YO6=`9@To#BNYmx(LX+mECfNfQlWrc(4fXc$^ zhUi-3Kp!0^1a9XAgA6P;TkTX=hol$tq~?bp#;$2D>4xe}fO|0yLnQq80yre|{-6Nw=huA>+BQGi)dq$6K_YuVgT?9nAgQKFlDHY_1 zn663!F(RUqOr%7nzit4Z3RiuKRk*uU@nKYN;#ke##*NJe%g`LgLsPN_;o zEjEbW)+DBFBz00UAp2-u(fTplkj5Yoe{Xtl0TQ}CW-uWr@{gaEtk>&Z7q(9$(myZ| zP;amlWAFOD0itPJV436R1T|M~^{bdA_TB=H;Rha7DNAlrHcgeQ95gMIW3f#ZZ*>!JZ#9X-p{-})f0Ta1S`KowS-jPfx+K{BRtnvU^%LB%a!zW zya~koa#jtEUd>|CeKj;z1&jk5f`JU3aEL3r41dE`&)`JsoqF=?taK;sEkMJfxxw0HC*!Yz!R;B@s_A4b7F!Ah)RxE zbPeQ(BX6o1?vA1S@Z2bwB}9~x74AwC3B+*1oe?VFQ~I6kmQ)}y6#Hn6G{1)PssyKI zN_(1=m$C9{XzLyRiKb+?dx0A=YDXR#ts}0dptKmMS`%34h;;z*XSC?*y`Jzq#IRW> ziV8-!0(n?Qi71|P^R%M`fzhzSoi8ha^o!-w{z{pt)Q=0|kjeNQ-fL8$G*ID#!Yqu;c=UU)11NNqE+Upq=bwu zfXnC8q{uLL-SBcfi?eAPAFmUy2usohTW~<)%iy9a(W)|;465GiszChzT}OxJz`l;7 zhc}EG4jB)PbW81D1KvFKfBQePk1YGgLT1z|3q7iGB@#$0+kMNXHKG1x#lS6Br#JR( zG#jg|ioh~w&A52LEji9};s0-v8IYs?ZyW zBwp%oc|b_Hs!`kfx#~39u|~yBhZm^sajict@<+6FCClfoVUk7+H-ufy&=XPW+*BH) z3QsO0RStB-qiUmNb+&WJ-z-S^c$5@}#B&Hps$85Ep5=m*Z)D-C$&yk`4RKnV@q)UZ z2rt7{7Q#NgZl3<7PqC-l7xN)?7||wvOmce#v{pl~(a<%V@Z6G-NK-zw*t12zFjl1( zARv3U{TSdyz-d_FE~Y9V2HWo*YL2h54K~8uUh87loIxQPeQ`A{1=s+6BcV9wi<=%f z#q&RDlp_HQWK{2L(`x%kix1z}*R*}OY<3K%0Hj_l7fKfb>5Ws#U0%57Ky1l!;40m# z7+RTp9#9Eyz0dz#a_kgSRBp^C@Yu3c5q&}bS#a{Pz%HY=;umy zkj(YL!~De$dCbH!Q-115Y)JffadvD2kd@{m8C-k`*P-Ad2+~Ww`mI?);WBJH_QX6=97`dfP2wd^m-#^4cHGcpkixP!)3^w-9#_&*QJKhE z`^VGxO>bwT8-h`gN!y`KUMdxli#G0XnA35&S*e^n7I>*$4(y;-t-jnUC<~iDpL02W z{a-NqGP7P^W)?V>tQZ;2f#y-Ez$Ud=t2?1^ES_PdVrf0k(-LjgwSWOB2axYTJ~B;F zs43&OC%ry*?&oX!>A)Ilal&P0UU}H$`K*^Ab866zF>4FSkyoDb!pQLAkB2U51}dJ? zBBQ$`?<5Xsg)a$locS}V1g7P`QjvZ~q^%=0^ zSn0YrDnty)U)_?Z?{(HI-5Jj~I)GgCi`^*ED14PUuP7PM$9M`(rXY~`>ht>ZDl`F+ z_nQVBoI{nXi`(i2lOjIDsR4|`3lTe!Xgjni!Itn-UF!>b_VpLIZvW>xaK^e0kK2_%K7_>bwTaV6WwI|)NIB`UY;fOV;-rwK|addszn?htyc0( z56j;)ZzYs3mp>7(vftc>u&MGSo|WaHR5oq0X3>b*IYMV;rt1luJua*FCEkjuzqvLiB7S|y_ntgMpu z=Z?0V94a-JaehFe0glKw0h^Y*)KF!}NAl(ffy|k&#zWOFyd9lP2o2gNe&?brqx%a1 zF(R!KAG!&1g3=)S93VL}t}_2nLc(l5f7@3fINgE(C!W;K^8m9UU2CD5B5mO19baXx zZPa2of~}7!Yv{7oGaDbO zZ4Ti}($shE#NmUqt!)5>%Fc&u)*5(+18i*zq+#ty@i1a>Wx(RGdgDm(-gIL-Ia{+^ zu?uJ^t^(&!RmyBh^80IkYB`!(pIEcqsvJqu=T}86M_uc_BuT|lGP7``G-!uqWuEGV zkW@4!Hw#TnhoH$>IT$jK8maLkPCbp%@S#pT1t7ZhkgVipsUc%>Pn+8_ql~H-#&oW5 zGMjO$R7L`T&T1XC4k%l_PC2&fBE170vQScdTimw)N%~VuVJq({t#t47)s%)y@Ne6= z?JtR7s-*zPMvhE+?c+VakX!xYLR@CyWFF@(URt#OfQ;x49!P1P`ene~0WP^~!Rk)7 z`fD>p!~Gh=#v%lgMxaSGxKu6r?_mVnt|&r3Rf7A-Eo9(RLzD}X zJlU9SAE|Y`HiFq;`m_!Kg6@&N1@6C`&A_)Ze}e?#3*}Q(XqSM%!qh8Sg;x_V1^7&a z7)P8Ti%ZYy9NW6FCCEQ~Yg1b09s554X(c7gh&zCpdCaN4R!?uM?fbVF_HOA+o~$|C)C1J4o|dac zY4@u4esWuF4c}967(m&UExy(Qe*0uKd=iEhb66NL?B5dH9o{8~o%OYU#$FM#m^nS| zsWJdrN!JeYO}_`1J0Eu4-MuWIBBPoGvj4d$>(RI z6dMz**N~Qyoz*&#fr#a?FrlVPxJY&#aUBpgoXq&@ly0U3Azox_Gf#tq6&Gz;s~grc z9ulq7Y(RN&P&ybT(pam8YQo`&y;ie3tvf<0G+fCFce|)W2@@E%Rh)si9X{dg~HWeRtm;s zM8qtUKLPkv-cLJXYM8zNI?c(+F)5w5;-U$30fY`TSb*g0m289{Xc-HUW}E%GWr%4e zI;=Q1nF*l&OegoYnPVj*X5BS_6wZI0A6dDNvSwc>SfS(`Zmh#ad1tt(6I6f2q3v2x z!BAq~>XaKQ^n$gyaZ5Z~T`NxQ>)kXw+3y)g(t0#dNA`M$12JVGxrchfyO9;0}^F?hb+ zk9q8RmcE@97VvQa&sVTJn`zPZ;5qPBn1;C@mEB7*-fj_T5p{uI)wtnk*tnNJdGw0jbsw_ zLr^`HD9#a7fEQb*Ii0P87t?Mn*X5H5*#s0JU%$w7q_ZgRle5n34Qf%cl#?h{h1wSc zJT_2i3y0GzYNQt_Nu(kry-~zzZgmAGQSV3hnuflGzXW28s}Co2~&cC)Y@TaAXY+fTy zN46Dg($L7{5Ht-DQ}I1RtX4XjkD3cb_!+4Y0L%j}WsA4(@CN8Y9-hAl$E88wSVTeW zHXwO@LYy0iOZzm<#jep9M%JLi4m3C{N}*i!!hpm*>4u~;YS;miq{oF0uoYy@k7(gY zItK*U*DQkpH+~~$0fy1>$Jm+EtiU+BIg4lkYv=2MPy*_juXdkDJDhPjJS3>LGT2AJu+@o?QsPdgVgotV?7cn4sN-*M-3 zO%ihUm2giL8~0T|%~kw;JQpuu@;3PJMiZso#v;-Vf)mO=F>1`2JnOhwn~eAfACt!; z`ri3HAnbcejk0l1?@RJ_A7lbnqsYObdO&#Q=LP%{BrVK8ZPPi!`_;{TR`_($iA4s+ZaX1hbS%@nH&l5r`gBiMN3~oBH^DFS= z3YIYqz*9WG5;zT>gZCfJ|G#p5RtAd7keFGMZ+lp+gMd1P4Q7#J4$!&INI1<`#wXe# zDo6@UoOP6&S0M75B{WMVBvk{x%35WJNwq}=(qG}}NhHBr5c5t9z;hOz1MC-CO2)k# z7ICJIF$DF=Oz_#hjgJrHC@@j%098m@~mEs<_NZ4(YQ#aYX9(=wUV-pb zb~2NDIcj!3+o?U4?AoX6jDv#1l0m0#(!*%rpR7l83F?V_dd|UU*1^5Xs`9naHHa{5 zLDXm8H48#wZg(l$FuV;9j`{FfIJO)`YQ_qtp*$ROjjTgkZ)@V5GR(~~RtjrL=mE}@!|1G3k z)PFY=cn1H2_{8x~Xdo;LkHZ5+^KYwbs#O!u&d(SCp{sd+%s+vYCZMQz4O^Q7BEVB1 zV^!phB}8n2#!-_R z@b33eA(H<4iPp9>8_tuBwA*18&>V^aiU1ybAbv~^$Fw$;puCxjc&uMc2r!zoHTp#9 zPS*H^kqkQ!n(j!t78fZw;zh}L$DIzsby*sr^WQei1kihTmfDnIj`}E@QADJ^R2ueg zY0XaCJdJ4(n}l@OaMLYeVUz^~8@|YveEU?*m)k&|gBO(zvOscCPES~>YYVYnYGSt3 z#p2ND7AN9?W9r`_cnt1WbuT|}%`ZLNEh$`bTam?wPbdB@M5^!$Rq(4{4Sp}wD^-Eb zzSW6NFgaPGN}aaZv0BlTo-}ug6OexOYCqy`N;s*b)>`aSJ5R9Mb-E*#m%;HzYbKj9 z80bGOT`IrrwD772@`dr!mTmO`ls)OOHBlInu`eN14mhwRA@gV~H)PCDH*}im}B(&KlgTdhMjcN&A z9rnLn9tunI5T8{hwrJ3^_?RJcPMw+9${ahy$4%tt1E2C6sLg1z(b(AXZD<@%r{8`m zK|UMgLkG_ST034MHIyo;AG&wgMpL7y(bQ;aG&PzUN>w$0o1>D-4&rF)yj1sWM%*}8 zpI#a=dfEDh%!uYN(=}3N#Dy@^F_?2~L(oC|4Kv%O@D5y|B7_1oq1`BsITiPDU7U)C z@%f!0y8kvLPY$h12=z_|ndiAe{FRz`Ep_op8Z&DEQ&k%ff7=-$8PA+B%9QnGMf_k5 zwj4yFFA;V$3orVSq>g2XTYA)xoC2(P12iV$EyDo}+pm)_rIA}$nu@A*?lX&s66B&e zOa}9mDP?7Z@JWV(QUQjOuOPIOV9KYQWn+Kc_`BAOD=%P86k zSh1_4<1VY5L`-Tv3`~Kt)dZugB>F zMpZ?vAW~F}B-~tg#8b?lMuJk)8Bt(nWJZw3lJK z^zV>~g@tE4&3LEji=$H8uL7F5Zre0&bkn#CP2-YHmSTZMxJymgNPT(^Zq4s#wx6X?zU&yLJ1xu}H1`gu^BG>EO8O`v;3MpWFWsg` z=btJno&Wh2#i9Q;X7n8gnqpiRFJ1^)nGXaYz+mfwCjG^GF;& z1sf5g!aXjcR}3J19FG5EjN;=GX6_dAL8;h#O8kUO6(#|rc$b6Ds6Cp~&diRAXbAHj zrSgxyF!`^@mDU5rZZ^KLmgN|ym-r0*Fh5N5o5zA1tMHPzc?{b%THmQc@cY0H+YdzI zU>L_1&lr`T;{sM%s58YMJ3+YuxxicjxJ1ymjo=J()=TS$fOz4CPLP-jhqfseVgMHo zVYs6Tat_+ml+z~*|7!qE6Wwx%1`C)Y`fb*YEx#aQ{mGc@52=yUgwP0$Ey=qhCS+Vl zecyYKwAK9j+Dz%Q2m@Oq*Qs2Kn}uNONy5{fplQ|Rv7pU}uDN;CWICcuyFrWy4{+9;!bst9x+MBWF!X#0T; z8>kKRcoA^Tei0pPIvRTeO+82qTr8v%$8H*mKm_%51|b@yFj=4L#%3M9qcHf?+)7yF zgLb>yfm(_d@8CHYy4>kI0R;l|nv#t^K&V&IV;EZuIG4Z!KlY5bh;37ddl`n;5q4cs z8;TIXI1CBJ|3(1Bqw-w3wyv;004fOZ*zx#G(GC)QGEN|s)3TlifSTISOPV$!Mj3f1 zLPbjLKy^Sckp9nm8INbW$mva-e}l6;?qYOc(evZZgoCz@m)TA6r7;kPa0C`)SNgIs z`}(NX-JEp!ruw|mej`SGqtdXypgmloGq}2~+;HDt`1-T3W*NjQb-d?z>Qj~eNFKCZ z&C=<>?2(^9C|JcHQ{r9fc&`ApVy;aW3F(fa+IGJRfH=WW3^IBwD|qb@an5aM5{?5M ztltJMtY0^MGAUFg@J^K64Wsx45Qsy@E!FRW)}X(QVDxXVPE3)}c(F5kZ~Ph*fbA%K z$q%YFsW*z4-A_G)_kYQokJtW>&cWM7b@z){*|9559wI20iKFhZ%POqwk`8lzJ95=e zI(03a*2ETZr@t=lR?0=;-zrJi0vZ;srAv2J_7S3r5=(Zuy=4upsSMuD@GWfh5&3Lo zyVV}1%&}kr$}@NiKob@AZg6NiHNgjrbw`ndB!j#F;tW~DB5)jLESU(ruNqVMj{e13o3D8-H`+E~etILt#Gm3V z&1ZBiTx&IruE{lFDAz^}<(jaTYemi36W`p?-Kth1ZY9J=?4_bdaIpCmqGi1BfMGHC zPcuD=SdZJ7oxvO)9e~FJ>sJu!h<^7fCt8zBB@XLZ-UjgP`{_$vT3l}s?-G}l$Qg-h zN2_syiSGMj_>RHiFf-(mz%#*RO74;p>+{Yp^Zbvb;VyiqVX@1Yntns}F7@>7@`PhL zUS^v?>Qen6%m8(n)>C5CH!8LL8O?gz8c$OZx#56IGM=Bjed5dsJBM*gsO`#Z5^;s) zaTT+14L_Q?K57h1);{L+amD#)Is6<{GxUtqbM+r%IPXjU_t7i%g*FeDr z!0_R%FE;(5DgDf9&kZo=Tpm%GU;=ph!Y==;^r}!fI-4^ZDxmtQfS z<3b9FU>PDn7}A<1Lj=Nz;wmyk+?OF@Qig~Iiaj!OfPe8QRq$wJky8NQ205e04(6L7 z$K4RMQf_#ENIW9p4Q#a|ciXT%oI{z$@&;!`+{O4A0HS7yLpUH9uNsUl+D32PtV8tC zp?E}$3gIzJXa@qfit&=TrOgDhW(Bp1#<7TFRJ9RbKp9+py`q<{qH&z8Gy}J>U15-% zFF@ecHd-tZ`x0o9 z+Td6?ns=D@7XTuL*fNgI_81k{TM6qPcKwh@xye#9<8Wm1x5kb5ZmBQD*PCvwuou)F zgtsXJ*k(5i2O%C&J+L&PT~Q=JN5r|18cfdFc<*;7_)?4=X5FLBSEg}ax>rw_7!yF^ zfcI!H@wv?8!{UgybwQ)Xs%Bv*j+&4-Mj#8VjO}^rB1oDE;2|=Q_PM12u)u z8oEzhGQ@QLu-Ts3pPHV66CkmphdsE{v^CSG&0gKOQIXNq8JHDklUgPRMXFw_(%|Y~ z^Z^9(*Xe-IY z;tKuaDn7+EL^pN4ZDg)Rqzfo$w?5OZ^<%^Pe}*_RMdN1fw(IJa zymRO&TcG|1_e&^ zz!88mf!2E3ehoMVaL&K-H;&6tlh2cJw3~9}&_w@7r;sxJo36~bmX}7hi`VQsg z?y`?_&(^zY;iwyX?PiFJoMWIC!)w(E{o<4Ao@qrd%X>G9AAW(8)%VhXX~@6xeX?pDCCyhmNeQS$^Cnqbz(ghyDSNr z7$)ABT&>tw1xBZ}CrBsYAu>f4eMs)EW&^(1uY&z@vx^EuB~>LCR5Q71$j`D?exa{) zwXAE^pVdg8-NY8*;7qigwZ$DF+C{tSKMg6Fn?1qg--Vz!O1kK~-xp0nZWW}9cc3D+ zLgCf`&HfI=3a=HiDK~ z1Oy#uH@H56C6AY8O=Wfhf?_y9QnXkimB|^F;{}CMrPgS5x@G`EFoI$@K~glsa=ai) zvZ88L2o^e$9AUymi1bF3Xfa~NiI*TzlDCqjNR=kNWR^GO{cWgG9xF#7qiY|HTIeQn z4`}_+$!7ikO}OXv}lrzU&9QbO*-o|LMS^71M=RX(b-` zI1w%4MXX5HQ`8YWTO-5ub->6-5ceCIy?|5pJ%d%~f3*JQg>`Si7%Xt~B8$&R(THI= ztd|v)NW&L*U67!mhfW;yi{vcWv@5(A^Hky{?$@3B&0p*{gXphN|L}MUMh7pve2z}q zzQ738X(m5DPjV3+DJu)KCHGn@Hk--{Qy((;_^*qeEzth}Bm6bRPxDmSs>AlBOVQ_$ zA=Z~67i%w0o&BQjimJa{8n~n3ifX))nyjm)D;>>}JhewWy*D-YT??C9?xkpjv7cBI zPem-MyDC3p-K>`Nl~t}BMqIa$Zn0avi~`grKTZFK3ARl5v(ynPlW7vmluO$+Zs$ zpLLL_{W$oqe;H_!*^o@QWWGx#UNY^H`7IepGC=aJ(n8G>5Bh6AL%(Ful3|dHiDZ~0 z-;xTzl4+LAnq=lBb3!solDRCIK*<2fluBkgk#FJ*6OtL1%$VexUO++eEfzQ~nQqA> zO9mwJIQG9J@6xjaJFszAv`&mY|(j$qktS$xfDn zQD+q1swk8;NmWPtj2P^D2T3LHifO4q|0r0kPvq6dOpM0nMG=UtyhX^Y{kxm~2``+E zq#`+2M1b{unbJ&$7wb?f5fkFu);%&Mskx&!cd6}zHV9-bUvO}T>v@gVqu-78+SC#p zayIM<9gx)J2r1oYaznbX$|2Ffkr%TRd3a>~#0#7kFz4khZA&WN zqE)G~6WFPhsUCJ;C(Kmg^wlyD@jTr{iY`y@-vW7N*Gf0M&Hyh`CN~A38#bsN@KbaJ z3mr=WE=J$?0FgeMnxqFS5ITvc!zUq?A;BXI%iW$I*B)1&aZB; zY+z}orLVzu#)03hw!owXMjNkSef}Z!~>b(G+Cr-5@Z=uO_>8-c_h)(E$pMD-s zcIW83&iq$ogbpkF1lI)^p0F&g{xmiw9-qW+YJ?&S1qioV-xt35)SM!R7fu^edu^U> zfSoeHZdaPsa}sXYDcUv2eOPwGvz8$Ew{!S?*QW|oJurBF3tX_Q1d?bni2SR6g_GS- zvsAV9XGv|Fschlmj(&xb5tHNi%YIPE%U{g%K`DArZ{7XJbVC0&cd;{)R{bxi|uZ-*;ng}`vdaK&(itPmc}A7NI3f7>a&i!9SN zb}bGSI^j31%U=w3>WEG9c5C0IJSFBx4KY$`2Q`>3dJ5P|7dZ!ap)A*{=FtC_xuoiu z7`sC2$98oiy$8P2)ho8vS0U{F`^m9`t+bg~{*+PckcxCUJ@39)P~v<*JYsxEX74RvBs>yYyoGoSP1*dJ2=P4&F2@L zJn89`n7^nj8`ir-KI(~_l!VOU(t3zK<=_jzL_Yz2dClCYry?cg*uSQ9PvmK>x6@OB z6tt8tps@Q64p_G1bEuv;4LNL)x#1I%j38%k?Z062EU?HD%9n6_#q}2d>#BN6MN68I zg)?$9Sy@W-Ct^el%w(`?`-kq(Rb`v5s-(M4utQ-0RFM?=+Jzt%gL_M#;t^y)#^?MN zN5X;~{rXH-Wk|1g@Pte+1X0w{f0+J`WtO`G@EuFuUU5@3@02;A?_C5^4r4@(w^Klk zg6Ku-<_i#aS1hA#&y|U>aW3V9{rxV)bBg+tc&B*N9wK(jt4#zfMfIr)FF@JJkxe&R ze_}3f)t{E07k#NRx7vxK|6G-uC0{RMaBDgbU%Rs$L&5!c8(Iy=f?bffice=}?_mjx z*Q5t}0iTq!?oDerjK?$j?>UiGH|rd{h4P?#fnHM1r-_)%0b&S8l<#d9B51N6`b^De11`>01(F zmpp89eE#KrZ_z!r`Wm}Ikjz_dsEuhZ8?CN1p+JEd64)rX6UI+DQ;(Mr?~ENnG46*G zhOFnPZG2vrPlF0mnwS&h6NJK27oVBKWSw>b?LiX$97NrBAu~LbLj>6>r~OWOFZt5( z+)I>kJ6HtZ;^|TZ*x7#X8_E~rg`|iKNs$>vLsG5&8D9vN7n{C<8~hV^VDd-^hrPUt0NE9cl7{nJC%0RL||zJhCM^|BIP$0sR;teWe&->IEVR=-^F(~ zX+;nUh58DD43#c1S98PVRr4zjx-1uLS_PjcA@HVwLYzQHm+-}k0)BtgGt2@Iy~6Uw zFHx$FT{27Dp1k_e_i@A7l{?qt=!fu%?TWJx!T)M-grOyOJjf}%3rYvi=jE2)S&w_M zN^Qafj{WnImQKRq5>ns~z>P*O|8%kTC3aJo3*j4L=GKEp0JB>B5Qz?DeUZAWAvWU7f_e*CAyC; z>_v)t|1;9H?*Vr{Au7U6o0R5#Nr1NU6)Px=`P=QjT5M$T1sc1v4$xNy(4fJ2SO-Sp zO23U&?P<4yTtDS85x{7u?~m_Mt$p1PS(9ND(wEb)?)~WbG~>U0r7+KsUcR z%3GVxF8E|^0d|SpzY)j)N$To)?Qpr%hi9XzW^PFO z-pl_JK7Tnld-U&(@AL>)rSE9L!*wr`uE$~SkCS~Z`JX>rJKcZNT`ytm*R<;B|5RCi zy3q8FYg?^{C@y-!EKLs$pz*0_a!vkkFL>=t#W*Q`rHkNT8dW27#v5e1N4vQ{>w07#h37t^h1g`ufHe#u&ok7#kPC_?wC z!u8~L7B|_~iW=SWtxl-)DMCHE1@Be4;1;lKR3bwo1xbwV`PGwIIh`gR+o^g0q_;5T z7PPMv{s566D_SzWt!uDED>0KZu@s3G%d^DwhHKQPC^z=%Gv!>1%Q{=~qnwj;xfE0iiHl&iK z_o}XpB{wtr_;@)bL@~9s(aGX2@H4-1vEyDQw=29eIo?aKhlS1N1yS3MqwGoF%27p+tR7Y!)TYayfRv80k!wV(p{)v?@p7@J}k&bV6vnRWw-Dt z`Q}~Es*&FE`X-Ihjaz>H$t_eM7O30GP?48GpKHQEK*S+_4`u(?p zcUSME;%bxLxetkSIvJOVR?jT(ewVc!T$Fp@P>2!=ABkdixyfZep}5^?b2lktbC8424FEdqoNL&4rJ|dQ_cgVszY+ zFJ4`yb9MA{Gs5+B9fYJ=Wb~pWw>A+K${$znbdV}hk;*Jjcu&<^?;n|>K-Dbs*pe!} zCZ_qMaPwx?eMQIZf<=7J*NMD;r?02$ASBHqqdZfaR-#b;xO%69RE3FDCOY9;?;lw~ z(LMRIJDpP0b@RnKCG=;6CCPacw`$@PY0ASb?d9^+NKsz*z$VRN>-L0fr%ehWQzy9> zFs;vc-Ao$N`#`*)dTShEU&*90QpnHFy{+R`<|{2F&78pO^^rpPH0tl9?`Mbngrb-i*+iMaO?ZQQRZskvY=p9_uFPKkeNZ z!Nv9~A4CfKQmAM&O{=?+b$a&n%rv`}=xI^c9qru_OdzVK0~$?)_e^|1A_$&!JW67g zAu`{IEo9tm1Kg)oq(qYpYl8Ud?Fl>{eK}!RLA%zO@l6t1>_@EW+65+zsaDE*rLIZn zdOp)UhQqyyMIs}Vp7-#Z&!T}d zmP>M-YN92A_3%Lyy(Cggd6%Ma1>JFPh<~DJs;<|3GOYCwM1Eb>+rw2s?ohc$%5S0} zkX)~h8=GXnCjr@hCRAB)Ww7Vm07Z+-I=g4B^6cNl7BVk`;Ug9lq?DuSOMO;efvacC zvIR-s@ZBx+rx!39IHOpTTCp5wvL!_QBQ5Wd$g_)J@vB!@m)$q7p8{_BQfrcRpdiQt zFn>;9%eLAZiM{oxC8XCi)NM*qyVNsz{9e(Z7 zA;ggcIvfvE0!K<$%iXo-v6aXQrbLo4GGzP>$$Gt_-Li>V*Xtch{fJh{C7nas#m9~2 zQje9t%8%1I(iE~JGLtcH#6xu$-t`HZoGn^@C?Y(nxwKV4!e}0e+B58tdsW%gp=aRQ zE11!bSa~|ttM@DwEmLYr8P6unPihKwsz@_A)dPJVpZ2W0FT?;DEwcACmG0BIYNP~& z>}XSyXkCZctKLhE6sg=xtHk>-wARMn5$YQyvR+-J_bfE}xo|;OxYR_693svaJt?7o zX|@ngi|Xos$=aXwDnpVy5+-IPd4;Y7v7L~duaN{zqQH_vnEd*v3(Wx zcjvnZn5Yk+&}Gbd&giW1h5;CFmKC~JA+d)db5GEeUF*JOr8cpb-#Tbh(Qk4DKhYT>wa+xn+uhQ=pB;0ynVWNEih0^$BXA`|QPB&xDb4C~C%(G=@9M&GMDLDF^4yQ)9 zb|M&&{+FL4{|;5w!<&~$!NWWYVyU}~YoQ9n@XJ&|b29Iif0@s8Vi^~mhFF-8Y)t&< zLLBj@@=N*UvtXUF-w3sBvyHwqB_VupS)#@QbYGuMX^^tko5MAxA0|!67vFc z7p|~=ghO-g7FS4poqH|DxQs=&#q>bn@>o*4<6KWwee3N0h|)gJ6B9lyL~wodxa^r) zSPJ38X{>9u5J`B{wp!AAWc_AwBe$l*^@MJxt3wxF0WVJa(D^=j^Wwhm@KknWpHrNC z^O)n@Nb*Wk!!wb9!q69{Y2?RUv7)vsaZiytDiby%0*1o>sw6$xrL~0yhZbI;b7c}q z=liVBi%CpWWqsaAqQjNjR0wb!Stas^w8Z7(ttUeW0}SSD~904Yq+ry5<=R_z z+ByTXJsAn%z|xK7@KR=4#Ics|aD?7^~5-*zWUphB?FfZa~PCJfS z4-Rn^t9g(X36bU%7c{Wx##%f@ksEcXW_{EdKleT?T=0W;_%14ypLYB#{oRSkh0EeJ*6m?=s%_>`7#6EG=nTi4qP_m$FN&D@>RoO~kUM z)NC?Qd~%aHIAYcB-G!+x!o<#BN-HY@GS$R|Szwa9!4XNGJ58F=lONCqxV{GXHxbm|2^PUN`1 zblgZ#0;5cT4dxdZU#C$DL1xr7S?;CtN7GBOyo{-3OV$lj)vCD1HO3_=Gg`u&UZR*0 zYt{P}Y+VYxDGvj%!M$AM)@E+}d<6Xk9$gcs#BS;;Xv3uzqsp%|LVBO#E0G*-kXB`&pTe)2q_4;JL+(im} zEyxsvYEMCfRDELYmfq#d7A2&M8Fjq>NrHEBn!P4nK@Dhcb$!$Q(et>-=%g|J2>^?E z7U-M4kQ>sh-G0ubi&l!!>vM5g)GI8+I$(!igCT%qU=3ydch|j}v@LwycE!O+jl)HJ z^Cw=iWXX~xOBQ_8YC#ty3r^oOk|hiN(x(015BzK$>O~yBsVK14cu?OX7X}AF_GEC! zUT<%xjlSVT69m!+mb063Y)lu-X4J=H1utvGCrI*8o2GQM($?%rZ={KlR$A!f0hogB z6?G|YO)1zrbU3(q69MX(6j3A;i#y#@5oBSys^pO^F`vC@nNsa^?v;x5o)tFD##l9^ zK8}a^&{<`Lm%8`MsUKG(r=cZ=#Cv6qSmqL2t{Dm$A4jdHRe0UvdFaw*@T@5kRrdht zExnz0%RBE@m)=wFdsAtt4ybyM4rc`bfmTmjp0_l=X~`*djRtys#Pp9bDQ~D_>Ry4Y zCmO4tihw${T@O7)qj5r*S|U~r%~`1DTx!VndJxtb zX_i&amNcVbW`i}#nf2@+TL`4rnK+>};k_H!B4Pan$!E{jGXB|e#viK{hD*p*ZyDvi znA@H)wT8b(YF(WVJnzJtjRe7^UQIoB+zXAM=vmWF=VQ071I;o~Z8aNMatxVD2)cgV zkN9PqiVs)(h@EJ$7C1+vh){o!1p?5K+PmGwd;S`=e)_Z@r@KW7_~?20eafgHy{i84 zuhp+LF3MYy1%E^o*DRA9L!hX%xE-eHsRum@^Tj7^>pQfGe%p7OE`0lkrcp6C6!R!J zij-FU4*_xHL#ZEq2IyWK&hntwaLPsJ%Z-6)OU3m^XO7_y&hL}^NTDc39b zeGF0i@v!FaE!K&jleEm<3FOcf&|x$-so5Xx@M(RMegNitjkJ*4Yfs+6l`;lE0N*+oDRta_efA(U0>-RgW_s&`*4>_XAA2mdPpfIq2WUNoxR=wWsWyPjBu$w}d0rV6xH%FZVt+K%riy-o0CcR(IJovL)~9`wlg|EC7J7w;{#a$Z*5I=6@;XQGf!48=l7h{1JD(9}cPiodneC z-?LZvmR^`;Dflbu$BuHWo<)72R94B(mQ%I9Y=QTDM;Dnb`}nLyzK4o~iS^4nL_~R* z_>-J56H8-NCQ%=XfNwbZ{UGsuWzSwmuBrSlpUJ>WLzGBu-1+Hf!bNOrQt7Gw@;Gg+ys$ z%bT`)&TJVKzb*W*8>hi;_Z=H>`2h$ zQqK(%K)f{XV-XmUe#1=t!QX8@vNVozlucZ_^UlxCL}JE91W53AzH<6mgijQHd)bd+ z`GVzXEg7vQw!etgJe9W}hh1GT`=6GFI8(fb9~zfTBM*g_u)^3fGll2m@-Bk`tY+gq zA81MXETuiif;#$J|Ft(3h|;+KLDn2gN&Yfvv}3(a*Aj!UqJUAueotA=3icNlpR!ZoZ_fdBQWWi#18t z6SSH-z91x#DA-Yp=#vKRF=hso85zCYRHF+3!ITtJoxLtd`J7n2DGjM41IfRnO54C5 zsUb6D(4j$Uh`iRCM`wvh2sK+FY?Y=|C$bxLrsU+O7^j*xD$KD!?$D}Jw;N>6L$*nU z8s5VL9Sx$wRLg`7GN7i2P@L(fs=HT=xS~)@^GeZ9h&`1u0|_LpbjnH_7R+I`HR2%- z^n8C*>wBNw(_&v{fR}R)J!ZU*+z3r89W)23KF>V3rTsZ+aB$>#&qI4p9L+p- z)pHs?NeQQiUt3WIB2dx5Ugg5+-Hici6nXs$xhc#tCfck_vhTzDVwav4Z@%bD@nHbs z;HO2Jn15KMMqSfOJx@p+@z-9r&tG=_C}mD8X6+Fq`m&d%B9|u;QI;XkF-~7miA<%~ zxEZ(ywITHFAs>tw_=NhH&!#_mo2Vl$cX@QtaR*z1|c zz5smJ2amuVc>83l<(|>s0s2gRBS5NA{wJy(A-|{27p(FLwt42&eBMOiE?RJl1Q`&% zFv~HEw;OVVop5%al=SZsKSNFnq(qDAJo>c0@5v*5+Q2qdshK8Dq`$YqMd}Olh);t? zU%|U(+9Jsjt)~Pvx9euQAG^`mdhR?)pDbO!=+$`7D}+yZ>jkKJsywDk79F=Svx&1v z8F{9aJfBg3QnFUFJJLu>m0GXwo2!Xd0Y|igpA#fQry!yQsUYv{y5c1+kgq-REHSla zO4l;jxbZN0WYZOcOZ}ePp4OS%xZIcZXnHo8TUCMgqpW(GZ$W6UBTqv#t&ReQ=$q40 zRrn0GZaJO}rux1b=*^n|&@XQvgkrF=sUk|b#$ zdx=EevR8sEw46> zW?Xi{IkbFdcFR~!aV|ngox>pyX|`nQ)}rld>{$^9jQTu$ij`IM=u?&#Miad_8#fFo z^?tL0Uwz|oEtkQY>Z{NdSAcq3)?S=9H&V%}39!SRay6r;P$V>}QdroGRFyeQ6nCQ6 zF%wEn%@7j4V8-~psaa!om} z{wwg9SOU~D83-9k}dmyoG8$U5k~z1(0rIgZ^oI5(kxH$AfE6REc8 zrVp`+;wK|*$NS?HIw19C5MZiR$J_ddTyJQ?>~5>ef0&f8TDtl7RC=(n_I4=PKJ}mP zC@?x;+!ZU44(dji8*Ch@9V-bVXby&x`@PhC+~*{Bt-^QUV*n05%1(}QXBq(PsgeXq z!7%F@c;h`}&Q^mO?%NUNGe`7Jey+Ov=TfGi(JpuP>aN#LQfE^s_^;UZM>vbPbLvjT z2s(_%Hjo6S`c!UT!Wk2mVp1E<_^MeWW1yk>jPZ-<->C8E2>;70=Jn3_Wp%10J#A;a zg)!MWL-(+)4;#+K+2}6#WnV)Gov9V)wlz%t5Hn%3mrTsy(po>v89D(AkA zzVT}jffu&ezNzv6KXpbRT7P2D>o@xj31nz#+2$4<1{tvLST;nXJsygKcBVuFJRl*L z=6-&qft3cPd-hy5$zR<@^Az!u%SPy9tbr^O)+{^|vanLz% zn!;2gkFLM`GwpPo$}Hl2>;3Jh1?k%;2l++`t2f^i=<651HI_Wli3eJ$mA z4(P7*oi$$n=}ce_2^?qf<(ih&w#h3(qwCL>5~d+ckaMfO+C{m!MjOaxzM=$g!>f9F z=R7uv?DSNxXm+ZWZdpt0b zvwi;d(mnq(FNW+n1N5m@>N*>fgnlgHk?0y9VE(gW;${AdUXR0=SEYBNDGoCSvN=?W zilV?~STiPb%J5$A26n(H0+-fGnPZhl_ciEz*YjE`WC|i#Yh~Evd4^nMcYVdc52@1Q z6Dz_aC8v#et!ayIYG#0G>qB?ZgSMUQ)GzU*xzt&CdFmP~C|5&fNZ*Rce0x?Dve8ql zEJl=Ln@Gnjy0gv;OB6SrA+pE8^$T#;${wXjv(zHk=X9BTxV`L@&To5r=X#dMUV`sU zJQ4}uOJ1U{J^53L?kPFel@kJP->d#DLEMJNzv3cLTu?7y4HO4@h-Im2qu9)ASi3_< zDAu43;(X?r5amajMMm}PNOIx>bEL6=Q$T8A+n}~e^w!6z2=oM}61AIR)`{$#Wd>u4W37IGAdgbb{aU7+A!Cg#1AS5INKt3G8=S{& z8j*a@SZ1g~!v1o2Vhmdo|EM1S{YKADc@vv@b)dKBh4OfU`BRfdI5bSH>%Qk@cSFi8 z`rWzaoOwLV6|quCK?%B3oUBv-b4Hboxc)Qp3Bk-A&3z@?;sDs-3?>jX*iZ5bIPAiV zR0O0+L(JU*3enM6*pg51H@1JGIKPYTPIwpZ>GQ@? zrX`{|v6}&OSViu&lgiB1PzbjK#Xpc${nA>-R(c9Y+K7rxJ?C&{(jwMMzCL>Kd1s!5 zXGBHDh>9P1O(kqf*IRFBY!0E2eJhV@P69>ECmC;^6a}GYK%e5KThF-CDa%K4mRsB} z>dyA6lc$Z&(tWc!4bh)~+#M;?=8{XdLa#7@V?r{?;ZS%1)-gGL0?D?y?=)MBr*MWq zwBDx5Ez)S5sq+Ib&K1*HjxcsPk+Z377{mYtl-v3Px(#UsqZ|A$b$}=jQ8_0$0cV;` zHEw$>VN>Y1>ocNZFU$lzFslV#y;2#W>avJrPKg3H**gaf_{8>oOe=AU{zYYc@<* zFUo@*>19XCLwLYau1XJ(8Et|ailmHJF{2J)vPe$6fjmEQkk#$ zTtZ96@sJ|?mD##%eGtAC%lwC-;WJD6%vyN9cA{{n#W2Fh*D0VuafZgUgJQ@X zc|f^WKQbTXm8J-euWoTT+c?3Y$?dRGUkT?X>z7Y6ef-P}|Cu@{8&+BR0jyIv+Q(k# zFDYJ%1T{kpojI$yu>Oo{N0sCwa8A_gPd_0B`6x?y$ACSI4nj(d z?w5<$@`I)S`e{jQft%3Y<6&nrT?TJE%U&)rQH}?%iA=dSJr)D(0dxh(3bv8Vm&qj# zYs*&^?E?X30=Os`D{@U5=L6M$4hmq>cZm%7OaX&HbNn>pN?Zj2Bq9oMaq@XUC^7g> zSOCxl18U8V1ID6+d^}`m+5?OyD*_0_b_|(7u+JMou@rAg>hH`xrwmPD(DUhu0Pr*k z1JqK15ip>z0!)%ocozV)qct$^3TuZ9W5GleCJTsD(Y6!-PTD;_mmnI32-9vp0D@Hu zkRe#k3J8s1$vhB+HUga@_4cAb-xihE>3O*5l|Cxhv@$a?PqX{3FGM7vqkJpcJ z!zxFGP8AX1r8m7>G;(~`5>MzxJq&|%RkxyNO#s(djc1Z_mn>+O_@~!zu?vrVojU)R z_+rL(WX~+~f`6*_W>3WTHb&&%18D{d55DM#9b)pKmRyr!*(qvrK7{bNj3!l4dGuCf zM0bQ#zk8kFkqGOIC4#(2LLvLx(K|alKG}nVd z%i3;p4cWJ*ld$J#Wp=%xN> z6oKNWUZ_UuieNKlL(1!ecn5}TNinGsdDtGq4{I?&j5-90#S9`9%t0BCml>X!s^>q1 z2!+V7V%8^lS+pY~LXdin`!X#)qd~#4gR$~s2EA<^PT{2|Xoc}hY+4RU@?>_C-aL_y z64vd67-Va;Z7A9{?LMj3H3Rf3KEWr}Zf zXO!UzQR^Mt_08*kvQqn_QJ+q_Ao=BY z@xN59Cm-{LBvWd&T1|B~W0$Em+?L+f7hH~IWWa)*-Tqz#lwruEqEVzunOTubw6Ss; ziWI6=vFs?i7vZ^pqHMcsJV~yJDQuw7K|~1^r5%(Nt5S4E4V*jyl0-@stlUA9Mot|( zeFBt7Ql(6tLX}EZtz16Kc5GQ(=k(Fdy16U3a%cdMKtevBc42~KM|P~>(S1lNKdIf# z|2=EuW9{zp^au+RdkQU!T?w1?^eZCwSMw!XM(SDn3mMz+BgMzMl&Mpw^8WDSyVKOj z%FNEt(i9d*xPU>MPgJ%klrsaB0y0qqjN<>Z=23vDq*qypO#V%|Qpvi7tC#SK9$TB+ zyGem7V2^+Rfxh#>3*&%}7EHmL~r zX1MEuP#^)pkm#`hlUAe2NOnVA$?cL;B@4+}E~nGsD2-`LW&Q4OG#g$Y$U!DRtHT~s z`G}_E=WARv!vT>5$b*4= z)Ady4A$zx4zCW-9kSVyr%g?DK2%MPQ&u$9@?(SYdPhKOtYG%O0?D1OFw0fIAJ6#$7fR41P9A#S+C@R%eUX5Vy^?vWz003+!q!^1)1Q7&iZ*zM`sZ8mQWJy(~X;r3+S+0IDB(~x7&R8GLtgpK=xZz#2fHmJvQ1NtZoJLiyM^(Q{C4e8R zH=!+ykxTIdb{X_3RicTCoqruqhGpT>RvD=a?-HrnZ{$BF9$mR@+|!j4(nB#9~C?Exx>DBt>qmP zG7LKh+BWibu|RsJ-)n>6$yfhzdrdu~*dH_lfM+G z=H_x}{KazT1?8TjpBU3QW#i7_k1P`~l%f~Iqu zB4kfN?&sX~iV7klC0Pj*X};v-jxXoZ2YnxhKSHsOV=7b>CJKAQ zYT7bBPg}0$d$JY?##(InKBb3_fIZ3pywJp23n61AEoJppQ~6I=v0VQZXpzEkgP8W4 z@&i734O9Ih&)L#`O__Vmp>Mg8c^X^WiwvuOQ{(>5-+S`*+xvL||3)~E=lP14CBq)= z1Ua53iTtE-e|Zj+TjD4$TR6KSwM@gl@S&IlI^}2Ef(!Y(^>yddi}>I4_aCO6ViKyV za|V)Gb}R1MLoh>BGRs4bS5no0n0zaGYk==!P@kB5t>+`UsZTB>?@dnde2RQE-B3k= zW^dDgi`&({bWJI}R8tlZ6hs{D@Gh>)`&CO06`giJJTB^#34%H?U3Dvd_g64pb(MQ0 zAfrznt5uA1P1M^`tHp$q*%L5U^(*c3H4wxQ#pCY?Zp$E5!V&#tx2NVCht_?aVbnS6 zUfl-B#Q)F&QKeMF5IJ^Guwn)nfr3a^2kmlq0#Ynn88vdCsc$RROr7fjIG_H@A{3h>@w1>3UuPVbgz+}hPrDtR<>UK^9b5=4d(m~4HJv@ z{1AegJ)uS&f=VnJMW~XQX~v+TN~LNS&IKU)cJSxcumu@&2q?1*IrR`oH5FNR7-+W~ zdHErVIV78VD5|?Gxpx`HEcI4vA!LR;QHKviix>AnGOa}>8WMR(nWUY{5lhRcWueF1 zwsNl<`ZU~bFk&$o+ZUaVYQGGKa$-JW7>L*#YtD}`tl2zbbkzMBDf)bJuBH%vf*$D! zo{pkL^!e3+MOR#gTWrRjJ_%y}k<8o|)tr}Iy&G=ZxPJhn5A3@{cJbkK7l6~_))IpbrI$=U0FmGrmsg*$OYgwkJhw7elcX(x?Ml-jj6hoNj9Txt}3aw6j%RFGuHi*^zI^S z*K9NKY&V?gg3WHT*=n?&+izS)mgMAvIDw)C3|crjf}{yls+c*0rVU*B7&?R~lor35snh@Xre47dCT$wFZsAL#Hir-H ziChkxOOEW*&BlH^`ZH2Z^ww6K0DE8bOJdFJ&1GWB%?B9ALyqkYPB~YWHbL$V+r%52 z=&MjxC{)Do9(?I4)D|l_nMhGL4Y!YfWAg!dX!+Y&{C$fQS@mSU^Av}R zspo?!3|0Z1 zb6Hpe9k4=SkeDZK1;x$DO@&zw8H(k(q53mkcFjcYomDaRX`Nv6xJ^nlGc=rRn*dLM*7SXKdDEvhd+XJeDI1-cm zSo@D~W`%CqJA4oUL=eGg^B23XdDz@cBB68eFsp)LUplW1uql8LME=nVgL6bzKE-9+>6vtgK zlK0ko1yqMhA~Uf})3fYIz6^b@s+2PO=hDr-6!HA3q(~hpM{`ljRG{&ckm!2Utva(@ z@O2iK)WCElj`MmVQ=N=7o(ru~DcY+hipsuqxj+N8zJA+ zZoUPZNG@ikdHnk;IqD}@njIXKa)eDNgDi^6bIlI+-R+|c{CMZu)HF3+@alK0{?+cB z%3X9pcV-Nr=>;;A1QnOE!xW}bG6^gDATI4QS3@HuCn+nfGuAu&(R41TuHGhJMeRHd z;f~FsAqJS?2Gxm2BPP>cp*~(XSL~Zgi##La21OoKJg}4SR<*vXxmCTd<>RoQFVh*8 z{^hYgItW#E(cZ%+k3Mg7Ld%-!BG^g>)g zs&`00TLrTS24GZmnMuk@OHwzX_7ZSOo|N?X&}IETJ?4+;@%ZSu@$OwoTnM9U$Gp>l z;8u9%S^D3EAi8|$J_I^e*ZIX(&nTo%m`-@^ykqLuG%C3<`jvJP4J9nnROgEt`5&`a z=^^}+UyI%D)(~Yv+;KjyiweH)Oi~;O@|Cz6B(WnH6a*xZyiVPRd4#y2sz%uM17b?b zF3DTUz>7#2rx2tb#_J@^r2mrmhdj{Mo3z;04Q}zGUFBe11x4El=mGf5a4+E%1|VCW z)UP#5E+bAV5}+dvc|T?|$ba$*P*@XkZC#*Lv4+J=HHV7PPeloV0TtGJNy-xh{B}B5A*i}+ps~1dyQ_&Jv|0J8GH^s>#UO$ z_IpCJ$9EWpPJCqbjGL+$OP6$v<6{zIkSko;=qpmjrAp+=_wWWN>o%paKkrzIaVbfQ zs8qwh1V8Pqu`Z8*_lZpT;FL~vC->qy@ITia8FK5EC=cVrGJeiBLyyYXBk|^{#Pr8kfHdiAsI+*v6C;G;Reo<=n^VM0^w>I;`ME~N5-D|9+T2dltM-~dAqt3_~xm^YwF_B_~Q^ukw- z?CyRx*`q!HLM|lk2ycTISiMxs(5c7~SHN98=f+LALsBRjCU@(;YeP-~SRF1qi!7Hj zk0S-VrU9U!n3_t0e{GY)JuCkzez5QiTcWorMxIpI49>7tm4n)4K6=}=y*t+>wqDYP z5w$NX!Cu`18>R-%Y)`KjY%fDDLL&e7fIZ~!odL>tF=fK#l%&$IFEPM!I{hcL2Lqhg zkJ0p<;AxtAb>;=VQNtzQyE~99&Igyymmw) zXF49EvcLimWy~T$EtN#SS{k{8MS+APBqfLa>Nr>|k2UMXybo3I?GAdEsTe*sA{pTu zBi4uaPp|rWef9aV$xDTFFGSB0g2R*m0wKlzh&(y-^TzE>{-zuu2tp7a7}FTWF^q0( z#wij;qx%wL4Egqy zT5M^TWHjV07R_0wJ!%B&I_V|E&zl8Kp{`P}8GzDW%FlC{SR8LXoB*$cT}0&%6F{ zZ2)Nw4o2MJlp4<%Yi)XB6F%8-v4~G%BEq!_Dg0%;x`>hiW;7km_!YuK1+!##JQ6() z3Z-K@&v1zat$FJ02z55eovcY^I>@-jV>h$d1w6lS=EhjWF(fP*WE+Qm9XSDFK7o`Z zTh+xNl3W<&Jx0@=`rxRnqCx=dob%}_;($E3XU^X7nFfj*qP2uGPecdWsVmQ8@r)X;>suw9JjTXqNLH>588D*TZJM(6In*@@QFx5T;GY`d&Ae{sX-@@yJr+7 zvbj2f7}W(HH?s1eS2@=RM-Q)PNNA*rxx9G%uAezOQ5Xru)*d-3)InQDYH?Dr z|4nEX9UG|T@DiPw3%w$isq4$KFk=_klJ-f-! ze$2X5=nUEyA(gTiv=mk0Va+ToXZvQHXYBlzax~MYX>onDwh8mIT&RBmp^tb}?POGw z9cYlkw99`WjPoW_&(#OCz_8m`V#(No^%v)#htndj6)cPIkf_VSh)N^(NZeq_Pylq2 zkSFK(x1mUp$8IFiltkCE1borW1>}vp)^Iv5j+%d#=*W?eHbCKG-eoCoQ_E4*4PSBN z77NHsUlH)xEi?D>g|N&GKDvYM#?C&HPO?@67Q+NKeNwIJmLE$qiC1(g&!k=DHxVST zi7k~WSbZ)1CX5l)qvdM;svCGqP|&$k$ib5}JNk^eQ++ER>FaC@HZKv$^%NdP-~5>d zjY?m0?PduQN>GqqkSI6TfO~ijqL_dK6F<1>$(1Y5_+%94h_Zj*k5@B%l^MpW#vrvf zOrt(6RX6ZD=I+hJ3gE>K{V?{VOFwXN`Y_7+eyDLJyM#T9Tc#c>P`W|jS?~w`PYv+@ zmQ4pRIFwBw!GKWFSRj!0dJq^84##aE7}e=tfq>9%uifl9dp~7p<^n^#1Qa%GCLAnQ zW`O6M1($Vbpb5l*)CCH_KfT4|i&NF3n?fsYwLVN&m~gRK zm;m0h7F?I0frk)aEK&i0yoqRkpeNE9Y}`<(=Ja$}Im|#9KL)$FcXEXp60^0GDTz z(u!VfFp0DXYLt?;5*%2Y7sQ-wf+)k)>!27n9P(0MP(?>M#HG-z$#YH*XVH$q&*FU< z9?l!X7ImKmb1op7lYHPoYI{d?S^eZI_ElOs7c%F<`1-W;@&tU|zAO=!nQ&ho(lt8F zUR?XX(^R$uK(1oaJ_@F-wTtvvF}hjz)XUw;pJ-oTPHn7|y>?*r9|%$*9|ABWL2Ww9 z*|E92BA;`hb3-RyXh$gU%t-rqC+v>!LS}+oyt842Br8*$Rnt=O#nM&{m2x0pFbJnzjGnkuxGCs%`k&KtvyTz$`b!{j~ifLXu$p>l{ovN zD^k%@&(F~?ojnnNW(DeUm`7`kM$6vjX=B`I=Z2S8ka{cpeYi1VK$5M1If%`d~Y8K30KB$^y2 zRt%2aLtpoIL#}v2B5kIGBp8-VZoL(;g!+DX$K!U=me2Z0CsPp};&E_V1XD?#@hsR? z@ky?#awXY=psUM{B%LL{d#fmVMTsZkKQcc8Cka?ZR}ft4AC*ChIz6=Plx_y8wVm_)-ubkMM|0 zz^L4ZKhF;{ZeTF9+tsm4N&_!8A?b~^_*Ztj18z^=`cIEc*Ht)(BwM?g_3muR-LQz| zCZ6B1us>72N#7MJI`VVjoQvoF&DHpesO#9))YfBQS>{i;;gMOa$0*WpN#XwJ*2@of z^dP%a-dqltdKo~EDaH|J3H6Ors4?{RNri&y!@vA?D05p%d&UyE4!}XDocbiL4`9{t;lI(_CF_6^)#>R8dof6G#a|Me=-RyT#{VvOS6xa|_Gg;R+u36UEX ze}Q%HMZL!kY@$HDBd(NvLIE*-`IIvVu6SvVbvIL97&VQVrn3@m;TFBei7{h|klGFb z2;rB2uB+&Y70eR~3K~GrxIh4q*s{DtC<*m$Sm{yi*n+DY!V!a;prL6>22|#@7aG9> zG1PqBUgMhLs=(go&Bn^v!k+5*p6PP$zo}Qvy8f!CN=b*kTy$oA29dO^h YlP_pm ztM9Q_f^Ju1U)kTCx1hKLRy|sob}2&j2&ldb*lu^!byk+hq!~}+_;u0>%whduh(~N+;TSmC{wFV7$q?xsq7HHtMXK!Bh!IC?>zVs z4&UzGhlt&#PF~ytjIoG2{EVzwA9Ayn;0-nF>|uE@byk1q(c@|?JQr1hR#*>F-g@|3 z?d-u%RhtFtnGy!$p9|0OCFNRm^z%%F8(4ACq<8yCY_Hc|D8qZYSm*17Mn{Q9zjQ7{ zWfdcpJ30>$V|E`dKo#O~);}W*c`NIB=ixeXAWr4{M&@5$vHbWzt2~(8LaUJGqAPVJ)q4w>fZ@NJ>GjaD5Za+z3T(KUb6?k6#p55W& zzdE(3wqIfzb5xdWH)5pQl}g&^b7iso(49RpBYsl+z&Ctcv$65{CqiX_R~PMr-M*qk zMelENM32kx+LqlCdk3_bIp9<3eCw7gFi=ub6zCs=GQ3KL9W)6`Ru|96ft0=62$k8| zVOV(-+H(-=L$ddL>u0g3Jh$w-J3c;QaKf~Qd8czNBf7Gtu6nwvZPy`U^C3&S1G(q1 zr$34x5bWzPFd;1p#28_0z$r$+P1q9wJL13yW(6F|N~Me%`>)4TzsrGQFYie+@f8cJCrV-T8JxXD=5 zJrdSonp)91%bh=&Jz=jb$AhL5qzRPPEoM!%zEg%Kmt)s!=7$I^i}Dw`EWjbBL`$Y& zk}*nY;`rIChMh(Yow>6GBp+SYeW}>tp|!=!bB2q%Z(B3b*gui-0a^NY>l{__6Ya62 z|71g`ABa3sAy;yce)@2JibjfE+luE(DnDt`T(ig~O7}|uBO2@wCOa5tDaE8xe?iZE zt*pzaU9d^Q#vuY#Fso`TJdBjVVM`O0B}y8JDJ3sq03iLV_Cq=+3<|t~4a7Kk^7_}M zNH1LGzJt|m%v3vaW3xW38?;gCe1DPq#a+l$Qf($O*d_&#;zzJb8F%kd4 zDm2=jD7%Ui)nEr}CGT*xE9D?Qe9XWs&QympaHq}Xm6%HuY?yKrlB9-#hh`B0F<~8|0QHjtaY~aZX{1O<$ID_nw3iwV4}3~!{1*h z#VcpV3ah`-sxSN*|IWk;5<3FpWmT36p}{ zN_fRy`tLdWq)NUmPTqv$i!{@<^C%1Q5fW#+0e~-Qe3UT7t$|rg0u`OtsXsEBvxhWS zE7|kU_1*MtPEU9)`p2tm)MQ!Iupaeq*!y==LwwzSuC(eJo=E9lU?9YVujY_ik7wBL z>uSFMtJbM;yuQ)g)uzol9JNzU&LKDMQyh`YBqq@dSYk;Fn*r-fb-UVd+oWboS)6RI zN2|l~sj}br`}EUPX6D@7{fErZ{Qohkhw&fq7yDmSZp3$HueoH@=YQ$*Lvn;hQ2X>$ zcbzb2G^_jOuIW2hZ1hJP{J8UDaeecZPiFakc<1av@zkwVXooy%qiXxkXjWg3$`WM7 z0n_HVx11ns0NDwuqwC2AhFV8Qzo$uWXHOJNH2)mKMzM9HUoEAxekuC}&whSue;Cdq z@UwHPG3oEs->kut2X3FEob+#Fcj*Y!&Yfp6coz;Y_lx;sfB8PvFSHl$J^jQ_t(kY! zfyvDbe-{Au-kw>)S1XmVQET`c7FZ7JOEUN!m5Nsdt;|OyylC0Ik$+f)W+k_l)ViLB z4(@E`kX%u~|3$T`@7)c~e|j{nZJWXO^pV1}(oi0(v95cK8=LF5CVe}m;N@2>1pGff C!H4<) literal 0 HcmV?d00001 diff --git a/showcase-next/public/images/btn-active-next-blue.svg b/public/images/btn-active-next-blue.svg similarity index 100% rename from showcase-next/public/images/btn-active-next-blue.svg rename to public/images/btn-active-next-blue.svg diff --git a/showcase-next/public/images/btn-active-next-gray.svg b/public/images/btn-active-next-gray.svg similarity index 100% rename from showcase-next/public/images/btn-active-next-gray.svg rename to public/images/btn-active-next-gray.svg diff --git a/showcase-next/public/images/btn-active-next.svg b/public/images/btn-active-next.svg similarity index 100% rename from showcase-next/public/images/btn-active-next.svg rename to public/images/btn-active-next.svg diff --git a/showcase-next/public/images/check.svg b/public/images/check.svg similarity index 100% rename from showcase-next/public/images/check.svg rename to public/images/check.svg diff --git a/showcase-next/public/images/close_btn.svg b/public/images/close_btn.svg similarity index 100% rename from showcase-next/public/images/close_btn.svg rename to public/images/close_btn.svg diff --git a/showcase-next/public/images/error.svg b/public/images/error.svg similarity index 100% rename from showcase-next/public/images/error.svg rename to public/images/error.svg diff --git a/showcase-next/public/images/info.svg b/public/images/info.svg similarity index 100% rename from showcase-next/public/images/info.svg rename to public/images/info.svg diff --git a/showcase-next/public/images/logo.ico b/public/images/logo.ico similarity index 100% rename from showcase-next/public/images/logo.ico rename to public/images/logo.ico diff --git a/public/images/logo/ipification-horizontal-black.svg b/public/images/logo/ipification-horizontal-black.svg new file mode 100644 index 0000000..bcbdae5 --- /dev/null +++ b/public/images/logo/ipification-horizontal-black.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/showcase-next/public/images/logo/vil.svg b/public/images/logo/vil.svg similarity index 100% rename from showcase-next/public/images/logo/vil.svg rename to public/images/logo/vil.svg diff --git a/showcase-next/runtime/config.d.mts b/runtime/config.d.mts similarity index 100% rename from showcase-next/runtime/config.d.mts rename to runtime/config.d.mts diff --git a/showcase-next/runtime/config.mjs b/runtime/config.mjs similarity index 88% rename from showcase-next/runtime/config.mjs rename to runtime/config.mjs index 688f0c7..a6f170a 100644 --- a/showcase-next/runtime/config.mjs +++ b/runtime/config.mjs @@ -97,24 +97,6 @@ export function loadRuntimeConfig() { throw new ConfigurationError("clients"); flows.add(c.user_flow); } - if (process.env.NODE_ENV === "production" && !process.env.APP_ORIGIN) - throw new ConfigurationError("APP_ORIGIN"); - if (process.env.APP_ORIGIN) { - try { - const u = new URL(process.env.APP_ORIGIN); - if ( - !["http:", "https:"].includes(u.protocol) || - u.username || - u.password || - u.search || - u.hash || - u.pathname !== "/" - ) - throw 0; - } catch { - throw new ConfigurationError("APP_ORIGIN"); - } - } validateFeatureEnvironment(); const locale = JSON.parse( readFileSync(path.join(appRoot, "config/locale.json"), "utf8"), diff --git a/showcase-next/scripts/check-config.mjs b/scripts/check-config.mjs similarity index 100% rename from showcase-next/scripts/check-config.mjs rename to scripts/check-config.mjs diff --git a/showcase-next/server.mjs b/server.mjs similarity index 67% rename from showcase-next/server.mjs rename to server.mjs index c274c2d..8a51296 100644 --- a/showcase-next/server.mjs +++ b/server.mjs @@ -1,8 +1,8 @@ import { createServer } from "node:http"; import { randomUUID } from "node:crypto"; +import { networkInterfaces } from "node:os"; import { fileURLToPath } from "node:url"; import next from "next"; -import { startSocketServer } from "./runtime/socket-server.mjs"; import { initializeConfigEnvironment } from "./runtime/config.mjs"; const appRoot = fileURLToPath(new URL(".", import.meta.url)); const dev = process.env.NODE_ENV !== "production"; @@ -11,7 +11,6 @@ const port = Number(process.env.PORT || 3002); const app = next({ dev, dir: appRoot, hostname: "0.0.0.0", port }); await app.prepare(); const handler = app.getRequestHandler(); -let sockets; const server = createServer((req, res) => { const requestId = randomUUID(), started = Date.now(); @@ -36,28 +35,36 @@ const server = createServer((req, res) => { delete req.headers[name]; } req.headers["x-showcase-peer-ip"] = req.socket.remoteAddress || "127.0.0.1"; - req.headers["x-showcase-realtime-ready"] = sockets?.ready() - ? "true" - : "false"; res.setHeader("X-Content-Type-Options", "nosniff"); res.setHeader("Referrer-Policy", "no-referrer"); res.setHeader("X-Frame-Options", "SAMEORIGIN"); return handler(req, res); }); -if (process.env.REDIS_URL) - sockets = await startSocketServer(server, { - redisUrl: process.env.REDIS_URL, - adapterKey: - (process.env.SHOWCASE_KEY_PREFIX || "showcase-next:development:") + - "socket.io", - }); -server.listen(port, "0.0.0.0", () => - console.log(`Showcase listening on ${port}`), -); +server.listen(port, "0.0.0.0", () => { + console.log(`Showcase listening on ${port}`); + if (!dev) return; + console.log(` Local: http://localhost:${port}/auth/login`); + const addresses = [ + ...new Set( + Object.values(networkInterfaces()) + .flatMap((entries) => entries || []) + .filter((entry) => entry.family === "IPv4" && !entry.internal) + .map((entry) => entry.address), + ), + ].sort(); + for (const address of addresses) + console.log(` Network: http://${address}:${port}/auth/login`); + if (!addresses.length) + console.log( + " Network: no LAN IPv4 address found. Connect to Wi-Fi or Ethernet and restart.", + ); + console.log( + " For QR authentication from your phone, open a Network URL on your computer and connect your phone to the same network. localhost on the phone points to the phone itself.", + ); +}); async function close() { const timeout = setTimeout(() => process.exit(1), 9500); timeout.unref(); - if (sockets) await sockets.close(); server.close(); server.closeAllConnections(); await app.close(); diff --git a/showcase-next/.dockerignore b/showcase-next/.dockerignore deleted file mode 100644 index 105b0c1..0000000 --- a/showcase-next/.dockerignore +++ /dev/null @@ -1,16 +0,0 @@ -node_modules -.next -.git -.env -.env.* -!.env.example -config/default.json -config/local.json -config/local-*.json -config/stage.json -config/production.json -tests -e2e -test-results -playwright-report -*.tsbuildinfo diff --git a/showcase-next/.gitignore b/showcase-next/.gitignore deleted file mode 100644 index f5ff4cc..0000000 --- a/showcase-next/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -node_modules/ -.next/ -.env* -!.env.example -config/default.json -config/local.json -config/local-*.json -config/stage.json -config/production.json -*.tsbuildinfo -test-results/ -playwright-report/ diff --git a/showcase-next/Dockerfile b/showcase-next/Dockerfile deleted file mode 100644 index bcefd19..0000000 --- a/showcase-next/Dockerfile +++ /dev/null @@ -1,29 +0,0 @@ -FROM node:24.22.0-bookworm-slim AS dependencies -WORKDIR /usr/app -COPY package.json package-lock.json ./ -RUN npm ci - -FROM dependencies AS builder -COPY . . -ENV NEXT_TELEMETRY_DISABLED=1 -RUN npm run build - -FROM node:24.22.0-bookworm-slim AS production-dependencies -WORKDIR /usr/app -COPY package.json package-lock.json ./ -RUN npm ci --omit=dev && npm cache clean --force - -FROM node:24.22.0-bookworm-slim AS runtime -WORKDIR /usr/app -ENV NODE_ENV=production PORT=3002 NEXT_TELEMETRY_DISABLED=1 -COPY --from=production-dependencies /usr/app/node_modules ./node_modules -COPY --from=builder /usr/app/.next ./.next -COPY --from=builder /usr/app/public ./public -COPY --from=builder /usr/app/config ./config -COPY --from=builder /usr/app/runtime ./runtime -COPY --from=builder /usr/app/scripts ./scripts -COPY --from=builder /usr/app/server.mjs /usr/app/package.json /usr/app/next.config.ts ./ -RUN mkdir -p /run/showcase-config && chown -R node:node /usr/app /run/showcase-config -USER node -EXPOSE 3002 -CMD ["node", "server.mjs"] diff --git a/showcase-next/e2e/login.spec.ts b/showcase-next/e2e/login.spec.ts deleted file mode 100644 index b000e63..0000000 --- a/showcase-next/e2e/login.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { test, expect } from "@playwright/test"; -test("configured flow tabs and preferences survive refresh without Bootstrap", async ({ - page, -}) => { - await page.goto("/auth/login"); - await expect( - page.getByRole("tab", { name: "PNV", exact: true }), - ).toBeVisible(); - await page.getByRole("tab", { name: "Login", exact: true }).click(); - await expect(page).toHaveURL(/#login$/); - await page.reload(); - await expect( - page.getByRole("tab", { name: "Login", exact: true }), - ).toHaveAttribute("aria-selected", "true"); - expect( - await page.locator('script[src*="jquery"],link[href*="bootstrap"]').count(), - ).toBe(0); -}); -test("blocked localStorage does not prevent configured flow selection", async ({ - page, -}) => { - await page.addInitScript(() => { - Storage.prototype.getItem = () => { - throw new DOMException("Blocked", "SecurityError"); - }; - Storage.prototype.setItem = () => { - throw new DOMException("Blocked", "SecurityError"); - }; - }); - await page.goto("/auth/login"); - await page.getByRole("tab", { name: "Identity", exact: true }).click(); - await expect( - page.getByRole("button", { name: "Anonymous Identity", exact: true }), - ).toBeVisible(); -}); diff --git a/showcase-next/e2e/phone.spec.ts b/showcase-next/e2e/phone.spec.ts deleted file mode 100644 index c0b2640..0000000 --- a/showcase-next/e2e/phone.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { test, expect } from "@playwright/test"; -test("phone verification validates input and works at mobile width", async ({ - page, -}) => { - await page.setViewportSize({ width: 375, height: 812 }); - await page.goto("/auth/login#pnv"); - const phone = page.getByLabel("Phone number", { exact: true }); - await expect(phone).toBeVisible(); - await phone.fill("2025550123"); - await expect( - page.getByRole("button", { name: "IP", exact: true }), - ).toBeEnabled(); - expect( - await page.evaluate( - () => document.documentElement.scrollWidth <= window.innerWidth, - ), - ).toBe(true); -}); diff --git a/showcase-next/e2e/qr.spec.ts b/showcase-next/e2e/qr.spec.ts deleted file mode 100644 index 475ef0f..0000000 --- a/showcase-next/e2e/qr.spec.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { test, expect } from "@playwright/test"; -test("desktop QR receives callback from independent phone browser session", async ({ - page, - browser, -}) => { - await page.goto("/auth/login#login"); - await page.getByRole("button", { name: "IP", exact: true }).click(); - await expect(page.getByRole("dialog")).toBeVisible(); - const link = page.getByTestId("qr-auth-link"); - const url = await link.getAttribute("href"); - expect(url).toContain("-qrcode"); - const phone = await browser.newContext(); - try { - const phonePage = await phone.newPage(); - await phonePage.goto(url!); - await expect( - phonePage.getByRole("heading", { name: "Success", exact: true }), - ).toBeVisible(); - await expect(page).toHaveURL(/\/user\/info$/); - await expect(page.locator("pre")).toContainText("browser-user"); - } finally { - await phone.close(); - } -}); -test("closed QR cannot navigate desktop on a late callback", async ({ - page, - browser, -}) => { - await page.goto("/auth/login#login"); - await page.getByRole("button", { name: "IP", exact: true }).click(); - const url = await page.getByTestId("qr-auth-link").getAttribute("href"); - await page.getByRole("button", { name: "Close", exact: true }).click(); - await expect(page.getByRole("dialog")).not.toBeVisible(); - const phone = await browser.newContext(); - try { - const p = await phone.newPage(); - await p.goto(url!); - await expect( - p.getByRole("heading", { name: "Success", exact: true }), - ).toBeVisible(); - await expect(page).toHaveURL(/auth\/login/); - } finally { - await phone.close(); - } -}); diff --git a/showcase-next/next.config.ts b/showcase-next/next.config.ts deleted file mode 100644 index 00f7c35..0000000 --- a/showcase-next/next.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { NextConfig } from "next"; -import { fileURLToPath } from "node:url"; -const config: NextConfig = { - serverExternalPackages: ["config", "geoip-lite"], - poweredByHeader: false, - logging: { incomingRequests: false }, - turbopack: { root: fileURLToPath(new URL(".", import.meta.url)) }, -}; -export default config; diff --git a/showcase-next/runtime/socket-server.d.mts b/showcase-next/runtime/socket-server.d.mts deleted file mode 100644 index 0822295..0000000 --- a/showcase-next/runtime/socket-server.d.mts +++ /dev/null @@ -1,5 +0,0 @@ -import type { Server } from "node:http"; -export function startSocketServer( - server: Server, - options: { redisUrl: string; adapterKey: string }, -): Promise<{ ready(): boolean; close(): Promise }>; diff --git a/showcase-next/runtime/socket-server.mjs b/showcase-next/runtime/socket-server.mjs deleted file mode 100644 index 81f9474..0000000 --- a/showcase-next/runtime/socket-server.mjs +++ /dev/null @@ -1,39 +0,0 @@ -import { Server } from "socket.io"; -import { createClient } from "redis"; -import { createAdapter } from "@socket.io/redis-adapter"; -export async function startSocketServer(httpServer, { redisUrl, adapterKey }) { - const pub = createClient({ - url: redisUrl, - socket: { - connectTimeout: 3000, - reconnectStrategy: (retries) => Math.min(1000, 100 + retries * 100), - }, - }); - const sub = pub.duplicate(); - pub.on("error", () => {}); - sub.on("error", () => {}); - await Promise.all([pub.connect(), sub.connect()]); - const io = new Server(httpServer); - io.adapter(createAdapter(pub, sub, { key: adapterKey })); - io.on("connection", (socket) => { - socket.emit("messages", { event_name: "connected", socket_id: socket.id }); - socket.on("init", (data, ack) => { - if ( - !data || - typeof data.state !== "string" || - !data.state || - data.state.length > 512 - ) - return; - socket.join(`auth:${data.state}`); - if (typeof ack === "function") ack({ joined: true }); - }); - }); - return { - ready: () => pub.isReady && sub.isReady, - async close() { - await new Promise((resolve) => io.close(resolve)); - await Promise.all([pub.close(), sub.close()]); - }, - }; -} diff --git a/showcase-next/src/app/api/config/route.ts b/showcase-next/src/app/api/config/route.ts deleted file mode 100644 index 59bc6b1..0000000 --- a/showcase-next/src/app/api/config/route.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { resolveContext } from "@/server/http/context"; -import { toPublicConfig } from "@/server/config/public"; -import { json, unavailable } from "@/server/http/response"; -export const dynamic = "force-dynamic"; -export function GET(request: Request) { - try { - const c = resolveContext(request); - return json(toPublicConfig(c.config, c.origin)); - } catch { - return unavailable(); - } -} diff --git a/showcase-next/src/app/api/geoip/route.ts b/showcase-next/src/app/api/geoip/route.ts deleted file mode 100644 index cb3c843..0000000 --- a/showcase-next/src/app/api/geoip/route.ts +++ /dev/null @@ -1,16 +0,0 @@ -import geoip from "geoip-lite"; -import { resolveContext } from "@/server/http/context"; -import { json, unavailable } from "@/server/http/response"; -export const dynamic = "force-dynamic"; -export function GET(request: Request) { - try { - const c = resolveContext(request); - let country = c.config.default_country_code || "rs"; - try { - country = geoip.lookup(c.clientIp)?.country || country; - } catch {} - return json({ country: country.toLowerCase(), ip: c.clientIp }); - } catch { - return unavailable(); - } -} diff --git a/showcase-next/src/app/auth/complete/route.ts b/showcase-next/src/app/auth/complete/route.ts deleted file mode 100644 index b0608c2..0000000 --- a/showcase-next/src/app/auth/complete/route.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { complete as GET } from "@/server/auth/web"; -export const dynamic = "force-dynamic"; diff --git a/showcase-next/src/app/auth/mobile/login/route.ts b/showcase-next/src/app/auth/mobile/login/route.ts deleted file mode 100644 index 0a453e1..0000000 --- a/showcase-next/src/app/auth/mobile/login/route.ts +++ /dev/null @@ -1 +0,0 @@ -export { mobileLogin as POST } from "@/server/auth/mobile"; diff --git a/showcase-next/src/app/auth/qrcode/complete/page.tsx b/showcase-next/src/app/auth/qrcode/complete/page.tsx deleted file mode 100644 index 72f635d..0000000 --- a/showcase-next/src/app/auth/qrcode/complete/page.tsx +++ /dev/null @@ -1,11 +0,0 @@ -export default function Success() { - return ( -

-
✓
-

Success

-

- You've been authenticated. Continue on your desktop. -

-
- ); -} diff --git a/showcase-next/src/app/auth/qrcode/error/page.tsx b/showcase-next/src/app/auth/qrcode/error/page.tsx deleted file mode 100644 index 787e52d..0000000 --- a/showcase-next/src/app/auth/qrcode/error/page.tsx +++ /dev/null @@ -1,11 +0,0 @@ -export default function Failure() { - return ( -
-

Failure

-

Authentication unsuccessful

-
- Try again - -
- ); -} diff --git a/showcase-next/src/app/auth/start/route.ts b/showcase-next/src/app/auth/start/route.ts deleted file mode 100644 index 46aab0b..0000000 --- a/showcase-next/src/app/auth/start/route.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { start as GET } from "@/server/auth/web"; -export const dynamic = "force-dynamic"; diff --git a/showcase-next/src/app/sms/auth/route.ts b/showcase-next/src/app/sms/auth/route.ts deleted file mode 100644 index b230863..0000000 --- a/showcase-next/src/app/sms/auth/route.ts +++ /dev/null @@ -1 +0,0 @@ -export { smsAuth as POST } from "@/server/ciba/handlers"; diff --git a/showcase-next/src/app/sms/token/route.ts b/showcase-next/src/app/sms/token/route.ts deleted file mode 100644 index 88aeb40..0000000 --- a/showcase-next/src/app/sms/token/route.ts +++ /dev/null @@ -1 +0,0 @@ -export { smsToken as POST } from "@/server/ciba/handlers"; diff --git a/showcase-next/src/app/ts43/auth/route.ts b/showcase-next/src/app/ts43/auth/route.ts deleted file mode 100644 index 37911c4..0000000 --- a/showcase-next/src/app/ts43/auth/route.ts +++ /dev/null @@ -1 +0,0 @@ -export { ts43Auth as POST } from "@/server/ciba/handlers"; diff --git a/showcase-next/src/app/ts43/token/route.ts b/showcase-next/src/app/ts43/token/route.ts deleted file mode 100644 index bc609a6..0000000 --- a/showcase-next/src/app/ts43/token/route.ts +++ /dev/null @@ -1 +0,0 @@ -export { ts43Token as POST } from "@/server/ciba/handlers"; diff --git a/showcase-next/src/features/login/showcase.tsx b/showcase-next/src/features/login/showcase.tsx deleted file mode 100644 index 2d6758d..0000000 --- a/showcase-next/src/features/login/showcase.tsx +++ /dev/null @@ -1,347 +0,0 @@ -"use client"; -import Image from "next/image"; -import { useEffect, useMemo, useState } from "react"; -import { - ArrowUpRight, - ShieldCheck, - Smartphone, - LogIn, - Fingerprint, -} from "lucide-react"; -import { Card, CardContent } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import type { PublicClient, PublicShowcaseConfig } from "@/shared/contracts"; -import { - flowGroups, - labelKey, - needsPhone, - type Tab, -} from "@/shared/flow-catalog"; -import { PhoneInput, type PhoneValue } from "@/features/phone/phone-input"; -import { QrDialog } from "@/features/qr/qr-dialog"; -import { ServerSelect } from "./server-select"; -import { InfoDialogs } from "./info-dialogs"; -import { LoginError } from "./login-error"; -import { usePreferences } from "./use-preferences"; -import { SmsDialog } from "@/features/sms/sms-dialog"; -import type { SmsInput } from "@/features/sms/use-sms-flow"; -import { useTs43Flow } from "@/features/ts43/use-ts43-flow"; -const tabIcons = { pnv: ShieldCheck, login: LogIn, identity: Fingerprint }; -function logoStyle(value?: string): React.CSSProperties { - const entries = (value || "max-width:100%").split(";").flatMap((part) => { - const colon = part.indexOf(":"); - if (colon < 0) return []; - const name = part.slice(0, colon).trim(), - value = part.slice(colon + 1).trim(); - return name && value - ? [ - [ - name.replace(/-([a-z])/g, (_, letter: string) => - letter.toUpperCase(), - ), - value, - ], - ] - : []; - }); - return Object.fromEntries(entries); -} -export function Showcase({ config }: { config: PublicShowcaseConfig }) { - const [sms, setSms] = useState(null); - const ts43 = useTs43Flow(); - const tabs = useMemo( - () => - (Object.keys(flowGroups) as Tab[]).filter((tab) => - flowGroups[tab].some((flow) => - config.clients.some((c) => c.user_flow === flow), - ), - ), - [config.clients], - ); - const servers = useMemo( - () => config.auth_servers.map((s) => s.id), - [config.auth_servers], - ); - const preferences = usePreferences(tabs, servers); - const [phone, setPhone] = useState({ - number: "", - valid: false, - country: config.defaultCountry || "rs", - }), - [country, setCountry] = useState(config.defaultCountry || "rs"), - [mobile, setMobile] = useState(false), - [error, setError] = useState(""), - [qr, setQr] = useState<{ - state: string; - url: string; - title: string; - } | null>(null); - useEffect(() => { - const resize = () => - setMobile( - window.innerWidth <= 768 || - /Android|iPhone|Mobile/i.test(navigator.userAgent), - ); - resize(); - window.addEventListener("resize", resize); - if (!config.defaultCountry && config.appEnv !== "stage") - fetch("/api/geoip") - .then((r) => r.json()) - .then((data) => setCountry(data.country || "rs")) - .catch(() => {}); - return () => window.removeEventListener("resize", resize); - }, [config.defaultCountry, config.appEnv]); - function start(client: PublicClient) { - setError(""); - if (!preferences.serverId) { - setError("Please select an auth server"); - return; - } - if (needsPhone(client.user_flow) && !phone.valid) { - setError(config.locale.modals.phone_invalid); - return; - } - if (client.user_flow === "pvn_sms") { - setSms({ - clientId: client.client_id, - serverId: preferences.serverId, - phone: phone.number, - }); - return; - } - if (client.user_flow === "pvn_sim" || client.user_flow === "login_sim") { - void ts43.start({ - clientId: client.client_id, - serverId: preferences.serverId, - phone: phone.number, - login: client.user_flow === "login_sim", - }); - return; - } - const isQr = !mobile && !["pvn_im", "login_im"].includes(client.user_flow), - state = crypto.randomUUID() + (isQr ? "-qrcode" : ""); - const params = new URLSearchParams({ - user_flow: client.user_flow, - server_id: preferences.serverId, - state, - }); - if (needsPhone(client.user_flow)) params.set("phone", phone.number); - const url = new URL("/auth/start?" + params, location.origin).href; - if (isQr) - setQr({ state, url, title: client.title || "Verify your identity" }); - else location.assign(url); - } - const section = config.locale.sections[preferences.tab], - flows = flowGroups[preferences.tab] || []; - return ( -
-
- -
-
-
- - Mobile identity - - {config.logoUrl ? ( - {config.locale.app.name} - ) : null} -

- {config.locale.app.name} -

-

- Explore phone verification, quick access and identity services - through your mobile network. -

-
- - Powered by IPification -
-
- - - { - preferences.selectTab(v as Tab); - setPhone({ number: "", valid: false, country }); - setError(""); - setQr(null); - setSms(null); - ts43.cancel(); - }} - > - - {tabs.map((tab) => { - const Icon = tabIcons[tab]; - return ( - - - ); - })} - - - -
-

{section?.title}

-

- {section?.description} -

-
- {preferences.tab !== "login" && ( - - )} -
- {flows.map((flow) => { - const client = config.clients.find((c) => c.user_flow === flow); - if (!client || (flow === "pvn_ipificator" && !mobile)) - return null; - const key = labelKey(flow); - return ( - - ); - })} -
- {(error || ts43.error) && ( -

- {error || ts43.error} -

- )} - {ts43.pending && ( -
-

- Waiting for SIM verification… -

- -
- )} -

- Choose a method to explore the authentication flow. -

-
-
-
-