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/.env.example b/.env.example new file mode 100644 index 0000000..c50eae0 --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +PORT=3002 +APP_ENV=development +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/.env.sample b/.env.sample deleted file mode 100644 index 305a108..0000000 --- a/.env.sample +++ /dev/null @@ -1,10 +0,0 @@ -PORT=3001 - -PLAY_INTEGRITY_ENABLED=false -PLAY_INTEGRITY_PACKAGE_NAME=com.example.demo -GOOGLE_APPLICATION_CREDENTIALS=/run/secrets/play-integrity-service-account.json -PLAY_INTEGRITY_REQUIRE_LICENSED_APP=false -PLAY_INTEGRITY_USER_FLOW=pvn_ip -PLAY_INTEGRITY_STATE_SECRET=replace-with-a-long-random-secret -PLAY_INTEGRITY_STATE_ISSUER=ipification-showcase -PLAY_INTEGRITY_STATE_AUDIENCE=ipification-android 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 2b562a3..262b912 100644 --- a/README.md +++ b/README.md @@ -1,90 +1,26 @@ # IPification Showcase -A demonstration application showcasing IPification's identity verification and authentication features. +A Next.js demonstration of IPification phone verification, login and identity flows. -## Features +## Start locally -- **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/app.js b/app.js deleted file mode 100644 index b0c313c..0000000 --- a/app.js +++ /dev/null @@ -1,88 +0,0 @@ -const express = require('express'); -const path = require('path'); -const session = require('express-session'); -const cookieParser = require('cookie-parser'); -const logger = require('morgan'); -const createError = require('http-errors'); -const config = require('config'); -const helmet = require("helmet"); -const fs = require('fs'); - -require('dotenv').config() - -const { deepMerge } = require('./utils/helpers'); -const { validateConfig } = require('./utils/validateConfig'); -const { createTs43AuthRateLimiter } = require('./middleware/ts43RateLimit'); -const { createAppLocalsMiddleware } = require('./middleware/appLocals'); -const { setupPlayIntegrity } = require('./bootstrap/playIntegrity'); - -// Load locale.json and merge with locale from default.json if exists -const localePath = path.join(__dirname, 'config', 'locale.json'); -const defaultLocale = JSON.parse(fs.readFileSync(localePath, 'utf8')); - -const locale = config.locale ? deepMerge(defaultLocale, config.locale) : defaultLocale; - -// Validate configuration on startup -validateConfig(config); - -const app = express(); - -app.use(helmet({ - contentSecurityPolicy: false, - crossOriginOpenerPolicy: false -})); - -app.set('trust proxy', 1); -app.locals.pretty = true; - -// app.use(nocache()); -logger.token('isoDate', () => new Date().toISOString()); -const isAssetRequest = (req) => { - const assetExtensions = /\.(css|js|map|png|jpg|jpeg|gif|svg|ico|webp|woff|woff2|ttf|eot)$/i; - return req.path.startsWith('/public/') || assetExtensions.test(req.path); -}; - -app.use(logger(':isoDate :method :url :status :response-time ms - :res[content-length]', { - skip: (req) => isAssetRequest(req) -})); -app.set('views', path.join(__dirname, 'views')); -app.set('view engine', 'pug'); - -app.use(session({ - secret: 'secret-session-key', - resave: false, - saveUninitialized: true, -})); - -app.use(cookieParser()); -app.use(require('stylus').middleware({ src: __dirname + '/public' })); -app.use(express.static(path.join(__dirname, 'public'))); - -// Shared request context must be available to every route below. -app.use(createAppLocalsMiddleware({ config, locale })); - -// Keep the feature's 16kb JSON parser and safe errors ahead of the general parsers. -setupPlayIntegrity(app, { config }); - -app.use(express.json()); -app.use(express.urlencoded({ extended: false })); - -const indexRouter = require('./routes/index'); -const userRouter = require('./routes/user'); -const authRouter = require('./routes/auth'); -const ts43Router = require('./routes/ts43'); -const smsRouter = require('./routes/sms'); - -app.use('/', indexRouter); -app.use('/user', userRouter); -app.use('/auth', authRouter); -app.use('/ts43/auth', createTs43AuthRateLimiter()); -app.use('/ts43', ts43Router); -app.use('/sms', smsRouter); - -// error handler -app.use(function (err, req, res, next) { - next(createError(500, err.message)); -}); - -module.exports = app; diff --git a/bin/www b/bin/www deleted file mode 100644 index caec3c0..0000000 --- a/bin/www +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env node - -/** - * Module dependencies. - */ - -const app = require('../app'); -const socket = require('../socket'); -const http = require('http'); -const logger = require('../utils/logger'); - -/** - * Get port from environment and store in Express. - */ - -const port = normalizePort(process.env.PORT || '3000'); -app.set('port', port); - -/** - * Create HTTP server. - */ - -const server = http.createServer(app); -const io = socket(server); -app.set('socket', io); - -/** - * Listen on provided port, on all network interfaces. - */ - -server.listen(port); -server.on('error', onError); -server.on('listening', onListening); - -/** - * Normalize a port into a number, string, or false. - */ - -function normalizePort(val) { - const port = parseInt(val, 10); - - if (isNaN(port)) { - // named pipe - return val; - } - - if (port >= 0) { - // port number - return port; - } - - return false; -} - -/** - * Event listener for HTTP server "error" event. - */ - -function onError(error) { - if (error.syscall !== 'listen') { - throw error; - } - - const bind = typeof port === 'string' - ? 'Pipe ' + port - : 'Port ' + port; - - // handle specific listen errors with friendly messages - switch (error.code) { - case 'EACCES': - logger.error(bind + ' requires elevated privileges'); - process.exit(1); - break; - case 'EADDRINUSE': - logger.error(bind + ' is already in use'); - process.exit(1); - break; - default: - throw error; - } -} - -/** - * Event listener for HTTP server "listening" event. - */ - -function onListening() { - const addr = server.address(); - const bind = typeof addr === 'string' - ? 'pipe ' + addr - : 'port ' + addr.port; - logger.log('Listening on ' + bind); -} diff --git a/bootstrap/playIntegrity.js b/bootstrap/playIntegrity.js deleted file mode 100644 index 1f14a60..0000000 --- a/bootstrap/playIntegrity.js +++ /dev/null @@ -1,36 +0,0 @@ -const playIntegrityService = require('../services/playIntegrityService'); -const { createPlayIntegrityAttemptService } = require('../services/playIntegrityAttemptService'); -const { createPlayIntegrityRouter } = require('../routes/playIntegrity'); -const dataStore = require('../lib/data_store'); -const { exchangeCodeAndGetUserInfo } = require('../utils/httpClient'); -const { isPlayIntegrityVerificationDomain } = require('../utils/playIntegrityDomain'); - -/** Mount the optional feature before the app's general body parsers. */ -function setupPlayIntegrity(app, { config }) { - if (process.env.PLAY_INTEGRITY_ENABLED === 'true') { - const { verify } = playIntegrityService.createPlayIntegrityService({ - packageName: process.env.PLAY_INTEGRITY_PACKAGE_NAME, - requireLicensedApp: process.env.PLAY_INTEGRITY_REQUIRE_LICENSED_APP === 'true', - timeoutMs: 10_000, - }); - const attemptService = createPlayIntegrityAttemptService({ - dataStore, - stateSecret: process.env.PLAY_INTEGRITY_STATE_SECRET, - stateIssuer: process.env.PLAY_INTEGRITY_STATE_ISSUER, - stateAudience: process.env.PLAY_INTEGRITY_STATE_AUDIENCE, - }); - app.use('/api/play-integrity', createPlayIntegrityRouter({ - attemptService, - verify, - exchangeCodeAndGetUserInfo, - expectedPackageName: process.env.PLAY_INTEGRITY_PACKAGE_NAME, - userFlow: process.env.PLAY_INTEGRITY_USER_FLOW, - verifyRequiredForRequest: (req) => isPlayIntegrityVerificationDomain( - req.hostname, - config.play_integrity_verify_domains, - ), - })); - } -} - -module.exports = { setupPlayIntegrity }; diff --git a/components.json b/components.json new file mode 100644 index 0000000..36c458a --- /dev/null +++ b/components.json @@ -0,0 +1,20 @@ +{ + "$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/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/API.md b/docs/API.md deleted file mode 100644 index d0f00aa..0000000 --- a/docs/API.md +++ /dev/null @@ -1,983 +0,0 @@ -# API Documentation - -Quick reference for API endpoints and usage patterns. - -## Table of Contents - -- [Quick Start](#quick-start) -- [Common Use Cases](#common-use-cases) -- [Essential Endpoints](#essential-endpoints) - - [Start Authentication](#start-authentication) - - [Handle Callback](#handle-callback) - - [Get User Info](#get-user-info) -- [Mobile Integration](#mobile-integration) -- [TS43/SIM Integration](#ts43sim-integration) -- [Configuration API](#configuration-api) -- [Play Integrity Backend Demo](#play-integrity-backend-demo) -- [Error Handling](#error-handling) -- [Advanced Details](#advanced-details) - ---- - -## Quick Start - -**Base URL**: `http://localhost:3000` (development) - -**Most Common Flow**: - -1. User clicks button → Redirects to `/auth/start` -2. User authenticates → IPification redirects to `/auth/callback` -3. App redirects to `/user/info` → Shows user data - -**Authentication**: Session-based (handled automatically) - ---- - -## Common Use Cases - -### I want to add authentication to my web app - -→ Use `/auth/start` → user authenticates → handle `/auth/callback` - -### I want to integrate with mobile app - -→ Use `/auth/mobile/login` with authorization code - -### I want to use SIM-based verification - -→ Use `/ts43/auth` → `/ts43/token` flow (see [TS43 Guide](TS43.md)) - -### I want to use SMS OTP verification - -→ Use `/sms/auth` → user enters OTP → `/sms/token` flow (see [SMS Flow Guide](SMS.md)) - -### I want to get app configuration - -→ Call `/api/config` to get auth_servers, clients, etc. - -### I want to protect an Android backend request with Play Integrity - -→ Enable `PLAY_INTEGRITY_ENABLED=true`, then call -`POST /api/play-integrity/verify`. See [Play Integrity backend demo](PLAY_INTEGRITY.md) -for Google Cloud setup and Android token lifecycle. - ---- - -## Essential Endpoints - -### Start Authentication - -**GET** `/auth/start?user_flow=pvn_ip&server_id=stage&state=abc123` - -Starts OAuth flow and redirects user to IPification. - -**Required**: - -- `user_flow`: Which flow to use (`pvn_ip`, `login_ip`, etc.) -- `state`: Request/result correlation value; the standard web callback does not compare it against session state - -**Optional**: - -- `server_id`: Which auth server (`stage`, `live`). Defaults to first server -- `phone`: Pre-fill phone number - ---- - -### Handle Callback - -**GET** `/auth/callback/:userFlow/:serverId` - -IPification redirects here after authentication. Your app handles this automatically. - -**Example**: `/auth/callback/pvn_ip/stage?code=xyz&state=abc` - ---- - -### Get User Info - -**GET** `/user/info` - -Shows authenticated user information (requires valid session). - ---- - -## Mobile Integration - -### POST /auth/mobile/login - -Exchange authorization code for user info (for mobile apps). - -**Request**: - -```json -{ - "client_id": "your_client_id", - "code": "auth_code", - "redirect_uri": "your_callback_url", - "server_id": "stage" -} -``` - -**Response**: User information JSON - ---- - -## TS43/SIM Integration - -For SIM-based verification, see dedicated [TS43 Guide](TS43.md). - -**Quick overview**: - -1. POST `/ts43/auth` → get digital_request -2. Call Credential Manager -3. POST `/ts43/token` → get user info - ---- - -## Configuration API - -### GET /api/config - -Get application configuration (auth servers, clients, etc.) - -**Response**: - -```json -{ - "auth_servers": [ - {"id": "stage", "url": "https://..."} - ], - "realm": "ipification", - "clients": [...] -} -``` - ---- - -## Play Integrity Backend Demo - -The IPification demo flow is enabled only when `PLAY_INTEGRITY_ENABLED=true`. -Create an attempt, verify its fresh Play Integrity token, then exchange the -IPification authorization code: - -```text -POST /api/play-integrity/attempt - -> attempt_id, backend-returned request_hash, expires_at -POST /api/play-integrity/verify - -> signed state, expires_at -setState(state), then start IPification -POST /api/play-integrity/token-exchange - -> decision: allow -``` - -### POST /api/play-integrity/attempt - -Creates an immutable authentication attempt. `phone_number` and `server_id` are -required; `server_id` is mandatory even when a default auth server exists. The -backend resolves and snapshots the configured `PLAY_INTEGRITY_USER_FLOW` client -and server, normalizes and hashes the phone number, and returns the hash Android -must use verbatim. - -**Request**: - -```json -{ - "phone_number": "", - "server_id": "" -} -``` - -**Response — HTTP 201**: - -```json -{ - "attempt_id": "", - "request_hash": "", - "expires_at": "" -} -``` - -The attempt and its transaction expire after 120 seconds. The response never -contains the stored client secret or normalized phone number. - -### POST /api/play-integrity/verify - -Verifies an Android Play Integrity Standard API token and checks that it was -created for the same immutable attempt received by the backend. - -**Request**: - -```json -{ - "attempt_id": "", - "integrity_token": "" -} -``` - -Android must request a fresh token for each attempt and send it only once. The -backend obtains the expected request hash from the stored attempt; the verify -request does not accept an action, phone number, or client/server configuration. - -Android and the backend must use this exact request hash: - -```text -base64url(SHA-256(UTF-8(action + "\n" + canonicalJson(payload)))) -``` - -`canonicalJson` recursively sorts object keys and preserves array order. The -server computes the hash from the immutable attempt snapshot, including the -normalized phone-number hash, attempt ID, client ID, and server ID. Android -supplies that exact string to the Standard Integrity API. - -**Successful response — HTTP 201**: - -```json -{ - "state": "", - "expires_at": "" -} -``` - -| Status | `decision` | Meaning | -| --- | --- | --- | -| 201 | state returned | Hash and configured integrity policy passed. | -| 400 | `deny` | Malformed request; Google is not called. | -| 403 | `deny` | Token decoded but did not meet policy. | -| 409 | `deny` | Attempt is expired or already used. | -| 503 | `unavailable` | Google credentials, network, decode, or timeout failure. The attempt is rejected. | - -Policy reason codes include `REQUEST_HASH_MISMATCH`, `APP_NOT_RECOGNIZED`, -`DEVICE_INTEGRITY_NOT_MET`, and (when strict licensing is enabled) -`APP_NOT_LICENSED`. Dependency reason codes are `GOOGLE_CREDENTIALS_UNAVAILABLE`, -`GOOGLE_DECODE_FAILED`, and `GOOGLE_DECODE_TIMEOUT`. - -The API never returns the submitted Integrity token, service-account -credential, Google access token, or raw decoded Google response. - -### POST /api/play-integrity/token-exchange - -After verification, Android must call `setState(state)` before starting -IPification. Send the returned authorization code and unchanged signed state: - -```json -{ - "code": "", - "state": "" -} -``` - -Successful response — HTTP 200: - -```json -{ - "decision": "allow", - "user_info": { - "sub": "", - "phone_number": "" - } -} -``` - -`user_info` contains the user information returned by IPification. Token -details are not returned by this endpoint. - -The backend verifies and atomically consumes the transaction before exchanging -the code with IPification. A state, attempt, or authorization code cannot be -reused. A failed upstream exchange returns HTTP 401 with -`IPIFICATION_EXCHANGE_FAILED`. - -### Security and operational limits - -- Attempts, transactions, and signed states have a 120-second TTL. -- The built-in `dataStore` is an in-memory, single-process store; it is not - shared between workers or instances and is lost on restart. -- Production must use Redis or a database with atomic claim/consume operations, - and bind the attempt and signed state to an authenticated user session or a - short-lived verification session. -- Never log phone numbers or other PII, request bodies, integrity tokens, - authorization codes, signed states, credentials, client secrets, or decoded - Google payloads. Logs may contain only request ID, endpoint, decision, reason - codes, and duration. - -**Server setup**: - -1. Link the app's Play Console configuration to its Google Cloud project and enable Play Integrity API. -2. Give the backend runtime identity permission to decode verdicts for that project. -3. Set `PLAY_INTEGRITY_PACKAGE_NAME` to the Android `applicationId`. -4. Provide Application Default Credentials using workload identity or a read-only secret mount; never commit a key file. -5. Set `PLAY_INTEGRITY_USER_FLOW` to the configured flow to use (for this demo, - `pvn_ip`), set `PLAY_INTEGRITY_ENABLED=true`, and restart the server. - -Verification is controlled by `play_integrity_verify_domains` in -`config/default.json`. The request hostname must exactly match a configured -domain to call Google Play Integrity; other domains bypass the Google verdict -call after the endpoint has claimed a valid attempt and received an integrity -token. An empty list bypasses verification for all domains. - -The full Android provider warm-up, credential guidance, and manual test -checklist are in [PLAY_INTEGRITY.md](PLAY_INTEGRITY.md). - ---- - -## Error Handling - -**Common Errors**: - -| Error | Cause | Solution | -| -------------------------------- | -------------------- | ------------------------------- | -| `Invalid server_id` | Server not in config | Check auth_servers in config | -| `Client not found` | Invalid user_flow | Check clients array in config | -| `server_id not found in session` | Session expired | User needs to restart auth flow | - ---- - -## Advanced Details - -
-Click to expand detailed endpoint specifications - -#### Detailed Authentication Endpoints - -#### Login Page - -**GET** `/auth/login` - -Display the login page with available authentication options. - -**Response**: HTML page (Pug template) - -**Query Parameters**: None - ---- - -#### Start Authentication Flow - -**GET** `/auth/start` - -Initiates the OAuth2/OIDC authentication flow by redirecting to IPification authorization server. - -For a step-by-step IP phone-verification walkthrough with implementation references, see [PNV via IP](PNV.md). - -**Query Parameters**: - -| Parameter | Type | Required | Description | -| ----------- | ------ | -------- | ---------------------------------------------------------------- | -| `user_flow` | string | Yes | User flow identifier (e.g., `pvn_ip`, `login_ip`) | -| `server_id` | string | No | Auth server ID (e.g., `stage`, `live`). Defaults to first server | -| `phone` | string | No | Phone number in E.164 format (e.g., `+1234567890`) | -| `state` | string | Yes | Correlates the authorization request and cached callback result; presence is checked | - -**Example Request**: - -``` -GET /auth/start?user_flow=pvn_ip&server_id=stage&phone=%2B1234567890&state=abc123xyz -``` - -**Response**: HTTP 302 Redirect to IPification authorization server - -**Flow**: - -1. Validates `server_id` and resolves to auth server URL (defaults to first server if not provided) -2. Validates user flow exists in configuration -3. Checks that state is present; otherwise redirects to `/auth/login` -4. Builds authorization URL with OAuth2 parameters -5. Redirects user to IPification authorization server - -**Callback URL Format**: -The redirect_uri sent to auth server will be: `/auth/callback/{user_flow}/{server_id}` - -**Error Responses**: - -| Status Code | JSON Error Code | Cause | -| ----------- | ---------------------------- | ----------------------------- | -| 400 | `INVALID_SERVER_ID` | Server ID not found in config | -| 400 | `NO_AUTH_SERVERS_CONFIGURED` | auth_servers array is empty | - ---- - -#### OAuth Callback - -**GET** `/auth/callback/:userFlow/:serverId` - -Handles the OAuth2 callback after user authentication. - -**Path Parameters**: - -| Parameter | Type | Required | Description | -| ---------- | ------ | -------- | ---------------------------------------------------------------- | -| `userFlow` | string | Yes | User flow identifier (e.g., `pvn_ip`, `login_ip`) | -| `serverId` | string | Yes | Auth server ID in the callback path (e.g., `stage`, `live`) | - -**Query Parameters**: - -| Parameter | Type | Required | Description | -| ------------------- | ------ | -------- | ------------------------------------- | -| `code` | string | Yes | Authorization code from IPification | -| `state` | string | No | Callback/result correlation key; a UUID is generated if absent; not checked against session state | -| `error` | string | No | Error code if authentication failed | -| `error_description` | string | No | Error description | - -**Example Requests**: - -``` -GET /auth/callback/pvn_ip/stage?code=abc123&state=xyz789 -``` - -Both path segments are required; `/auth/callback/login_ip` does not match the registered route. - -**Response**: - -- Success: HTTP 302 Redirect to `/auth/complete?state=xyz789` -- QR Code Flow: HTTP 302 Redirect to `/auth/qrcode/complete` or `/auth/qrcode/error` -- Backchannel Flow: HTTP 200 (empty body) -- Provider-reported `error` or `error_description`: HTTP 302 Redirect to `/auth/login` with a fixed session error message -- Token exchange/userinfo failure: HTTP 400 with the caught error message - -**Flow**: - -1. Extracts `serverId` from the required URL path -2. Resolves the auth server and configured client -3. Reads code and state; no local comparison to an original session state is performed -4. Exchanges authorization code for access token (using same auth server) -5. Retrieves user information using access token -6. Stores user information in data store -7. For QR code flows, emits Socket.io event to desktop browser -8. Redirects to completion endpoint - -**Headers**: - -- `ip-backchannel-im-auth`: Set to `true` for server-to-server IM flows - -**Error Responses**: - -| Status Code | Error Message | Cause | -| ----------- | ---------------------------- | ----------------------------- | -| 400 | `Invalid server_id: 'xyz'` | Server ID not found in config | -| 400 | `No auth servers configured` | auth_servers array is empty | - ---- - -#### Complete Authentication - -**GET** `/auth/complete` - -Completes the authentication flow and retrieves stored user information. - -**Query Parameters**: - -| Parameter | Type | Required | Description | -| --------- | ------ | -------- | --------------- | -| `state` | string | Yes | State parameter | - -**Example Request**: - -``` -GET /auth/complete?state=xyz789 -``` - -**Response**: HTTP 302 Redirect to `/user/info` when a result is found, or `/` after all cache reads miss - -**Flow**: - -1. Retrieves user information from data store using state -2. Polls data store (up to 5 attempts with 1-second delay) -3. Sets session variables (`isAuthenticated`, `userData`) -4. Redirects to user info page - -The session flag marks completion of the demo flow. It is not gated on `phone_number_verified`; see [verification results and current limitations](PNV.md#verification-results-and-current-limitations). - ---- - -#### QR Code Success Page - -**GET** `/auth/qrcode/complete` - -Displays success page after QR code authentication. - -**Response**: HTML page (Pug template) - ---- - -#### QR Code Error Page - -**GET** `/auth/qrcode/error` - -Displays error page if QR code authentication failed. - -**Response**: HTML page (Pug template) - ---- - -#### Mobile Login - -**POST** `/auth/mobile/login` - -Mobile-specific endpoint for exchanging authorization code for user information. - -**Request Headers**: - -``` -Content-Type: application/json -``` - -**Request Body**: - -| Field | Type | Required | Description | -| -------------- | ------ | -------- | ---------------------------------------------------------------- | -| `client_id` | string | Yes | OAuth client ID | -| `code` | string | Yes | Authorization code | -| `redirect_uri` | string | Yes | Callback URL (must match OAuth redirect) | -| `server_id` | string | No | Auth server ID (e.g., `stage`, `live`). Defaults to first server | - -**Example Request**: - -```json -{ - "client_id": "webclient2", - "code": "authorization-code", - "redirect_uri": "https://example.com/auth/callback/pvn_ip/stage", - "server_id": "stage" -} -``` - -**Response**: - -```json -{ - "sub": "user-id", - "phone_number": "+1234567890", - "phone_number_verified": "true" -} -``` - -**Status Codes**: - -- `200`: Success - Returns user information -- `400`: Invalid server_id or missing required fields -- `401`: Client not found or authentication failed - ---- - -#### Server-to-Server Sign-In - -**POST** `/auth/s2s/signin` - -Server-to-server sign-in endpoint for retrieving user information. - -**Request Body**: - -```json -{ - "state": "state-parameter" -} -``` - -**Response**: - -```json -{ - "sub": "user-id", - "phone_number": "+1234567890", - ... -} -``` - -**Status Codes**: - -- `200`: Success -- `401`: User information not found - ---- - -### TS43 Protocol Endpoints - -#### TS43 Authentication - -**POST** `/ts43/auth` - -Initiates TS43 (SIM-based) authentication flow using CIBA (Client Initiated Backchannel Authentication). - -**Request Body**: - -```json -{ - "login_hint": "+1234567890", - "carrier_hint": "carrier-code", - "client_id": "webclient3", - "operation": "VerifyPhoneNumber", - "scope": "openid" -} -``` - -**Parameters**: - -| Parameter | Type | Required | Description | -| -------------- | ------ | -------- | -------------------------------------------------------- | -| `login_hint` | string | No | Phone number hint | -| `carrier_hint` | string | No | Carrier code hint | -| `client_id` | string | Yes | Client identifier | -| `operation` | string | No | Operation type (`VerifyPhoneNumber` or `GetPhoneNumber`) | -| `scope` | string | No | OAuth2 scope (defaults to client scope) | - -**Response**: - -```json -{ - "auth_req_id": "auth-request-id", - "nonce": "nonce-value", - "digital_request": { - "protocol": "openid4vp-v1-unsigned", - "data": { - "response_type": "vp_token", - "response_mode": "dc_api", - "nonce": "nonce-value", - "dcql_query": { - "credentials": [...] - } - } - } -} -``` - -**Status Codes**: - -- `200`: Success -- `401`: Client not found -- `500`: Server error - -**Flow**: - -1. Validates client configuration -2. Calls CIBA auth endpoint to get `auth_req_id` -3. Calls DCQL endpoint to get credential query -4. Returns digital request structure for mobile app - ---- - -#### TS43 Token Exchange - -**POST** `/ts43/token` - -Exchanges VP token for access token in TS43 flow. - -**Request Body**: - -```json -{ - "vp_token": "vp-token-value", - "auth_req_id": "auth-request-id", - "client_id": "webclient3", - "nonce": "nonce-value" -} -``` - -**Response**: - -```json -{ - "sub": "user-id", - "phone_number": "+1234567890", - ... -} -``` - -**Status Codes**: - -- `200`: Success -- `401`: Client not found -- `500`: Server error - -**Flow**: - -1. Validates client configuration -2. Calls callback endpoint with VP token -3. Exchanges auth_req_id for access token using CIBA grant type -4. Retrieves user information -5. Sets session and returns user info - ---- - -#### TS43 Log - -**POST** `/ts43/log` - -Logging endpoint for TS43 debugging. - -**Request Body**: - -```json -{ - "data": {...} -} -``` - -**Response**: `"OK"` - ---- - -### SMS Flow Endpoints - -See [SMS Flow Guide](SMS.md) for full documentation. - -#### POST /sms/auth - -Starts CIBA with SMS channel. Auth server sends OTP to the given phone. - -**Request Body**: `client_id`, `server_id` (optional), `login_hint` (phone), `scope` (optional) - -**Response**: `auth_server`, `auth_req_id`, `nonce` - -#### POST /sms/token - -Submits user-entered OTP to auth server callback, then exchanges for token and user info. - -**Request Body**: `code` (OTP), `auth_req_id`, `client_id`, `nonce`, `server_id` (optional) - -**Response**: User info (e.g. `sub`, `phone_number`, `phone_number_verified`) and `auth_server` - -#### POST /sms/log - -Optional logging for SMS flow debugging. **Request Body**: `{ "data": {...} }`. **Response**: `"OK"` - ---- - -### Utility Endpoints - -#### App Config - -**GET** `/api/config` - -Returns safe application configuration for the frontend (client secrets are removed). - -**Response**: - -```json -{ - "realm": "ipification", - "auth_servers": [ - { - "id": "stage", - "url": "https://api.stage.ipification.com/auth" - } - ], - "clients": [ - { - "user_flow": "pvn_ip", - "client_id": "your-client-id", - "title": "IP Phone Number Verification", - "scope": "openid ip:phone_verify" - } - ] -} -``` - -**Flow**: - -1. Reads config from `res.locals` -2. Strips `client_secret` from each client entry -3. Returns `auth_servers`, `realm`, and safe `clients` - ---- - -#### GeoIP Lookup - -**GET** `/api/geoip` - -Returns country code based on client IP address. - -**Response**: - -```json -{ - "country": "us", - "ip": "192.168.1.1" -} -``` - -**Flow**: - -1. Extracts client IP from request -2. Performs GeoIP lookup -3. Returns country code (lowercase) and IP address - ---- - -#### User Information - -**GET** `/user/info` - -Displays authenticated user information (requires valid session). - -**Response**: HTML page (Pug template) - -**Session Requirements**: - -- `isAuthenticated`: Must be `true` -- `userData`: User data object - -**Status Codes**: - -- `200`: Success (renders info page) -- `302`: Redirects to `/` if not authenticated - ---- - ---- - -## WebSocket Events (Socket.io) - -### Client Events - -#### Initialize Connection - -**Event**: `init` - -**Payload**: - -```json -{ - "state": "state-parameter" -} -``` - -**Description**: Joins client to a room channel for receiving authentication updates. - -**Channel Format**: `auth:{state}` - ---- - -### Server Events - -#### Connection Confirmed - -**Event**: `messages` - -**Payload**: - -```json -{ - "event_name": "connected", - "socket_id": "socket-id" -} -``` - -**Description**: Sent immediately after client connects. - ---- - -#### Authentication URL - -**Event**: `messages` - -**Payload**: - -```json -{ - "event_name": "url", - "url": "http://example.com/auth/complete?state=xyz789" -} -``` - -**Description**: Sent when QR code authentication completes. Desktop browser should navigate to the provided URL. - ---- - ---- - -## Error Handling - -### Error Responses - -All endpoints follow standard HTTP status codes: - -- `200`: Success -- `302`: Redirect -- `400`: Bad Request (invalid parameters) -- `401`: Unauthorized (authentication failed) -- `404`: Not Found -- `500`: Internal Server Error - -### Error Format - -For JSON responses: - -```json -{ - "error": "Error message", - "status": 400 -} -``` - -For HTML responses, errors are displayed in modals or redirect to login page with error message in session. - ---- - -## Rate Limiting - -Currently, no rate limiting is implemented. Consider implementing rate limiting for production deployments. - ---- - -## Security Considerations - -1. **State Parameter**: Always validate state parameter to prevent CSRF attacks -2. **Secret Keys**: Keep notification secret keys secure -3. **HTTPS**: Use HTTPS in production -4. **Session Security**: Configure secure session cookies -5. **Input Validation**: Validate all input parameters - ---- - -## Examples - -### Complete Authentication Flow - -```bash -# 1. Start authentication -curl "http://localhost:3000/auth/start?user_flow=pvn_ip&state=abc123" - -# 2. After user completes authentication, callback is called automatically -# 3. Complete authentication -curl "http://localhost:3000/auth/complete?state=abc123" - -# 4. View user info (requires session cookie) -curl -b cookies.txt "http://localhost:3000/user/info" -``` - -### Mobile Login Flow - -```bash -# Exchange authorization code for user info -curl -X POST http://localhost:3000/auth/mobile/login \ - -H "Content-Type: application/json" \ - -d '{ - "client_id": "webclient2", - "code": "authorization-code", - "redirect_uri": "https://example.com/callback" - }' -``` - -### TS43 Flow - -```bash -# 1. Initiate TS43 authentication -curl -X POST http://localhost:3000/ts43/auth \ - -H "Content-Type: application/json" \ - -d '{ - "login_hint": "+1234567890", - "client_id": "webclient3", - "operation": "VerifyPhoneNumber" - }' - -# 2. Exchange VP token (after mobile app completes) -curl -X POST http://localhost:3000/ts43/token \ - -H "Content-Type: application/json" \ - -d '{ - "vp_token": "vp-token", - "auth_req_id": "auth-req-id", - "client_id": "webclient3", - "nonce": "nonce-value" - }' -``` - -
diff --git a/docs/APP_CONFIG_JSON.md b/docs/APP_CONFIG_JSON.md deleted file mode 100644 index 0170605..0000000 --- a/docs/APP_CONFIG_JSON.md +++ /dev/null @@ -1,159 +0,0 @@ -# app_config.json Reference - -This document describes `config/app_config.json`, which defines the default UI configuration returned by `GET /api/config`. The server deep-merges this file with any overrides provided via `config/default.json` (or other `config/*` environment files). - -## Table of Contents - -- [Load and Override Order](#load-and-override-order) -- [Field Reference](#field-reference) - - [Top-level Fields](#top-level-fields) - - [app_config](#app_config) - - [default_environment](#default_environment) - - [branding](#branding) - - [main_screen.locale](#main_screenlocale) - - [pnv_screen.locale](#pnv_screenlocale) - - [ussd_screen](#ussd_screen) - - [global_locale](#global_locale) -- [Examples](#examples) -- [Related Documentation](#related-documentation) - ---- - -## Load and Override Order - -1. The server loads `config/app_config.json` as the baseline. -2. If `config/default.json` contains an `app_config` block, it is deep-merged into the baseline. -3. The merged object is returned in `GET /api/config` as `app_config`. - -Practical implication: to override only one field, put it under `app_config` in `config/default.json`. - -Example override: - -```json -{ - "app_config": { - "app_config": { - "min_supported_version": "2.1.1" - }, - "branding": { - "app_name": "My Demo" - } - } -} -``` - ---- - -## Field Reference - -### Top-level fields - -- `config_version` (string): Version of the config schema. Use for internal tracking. -- `app_config` (object): App versioning and update/maintenance settings. -- `default_environment` (string): Default environment label used by the client (`sandbox`, `production`, `custom`). -- `branding` (object): Branding and color overrides for the client UI. -- `main_screen` (object): Locale strings for the main screen. -- `pnv_screen` (object): Locale strings for the Phone Number Verification screen. -- `ussd_screen` (object): Locale strings and config for USSD verification. -- `global_locale` (object): Global labels used in the UI. - ---- - -### app_config - -- `min_supported_version` (string): Minimum app version allowed to run. Clients below this should force-update. -- `latest_version` (string): Latest available version for update prompts. -- `force_update` (boolean): When `true`, clients should block usage until updated. -- `update_message` (string): Message shown when update is available or required. -- `maintenance_mode` (boolean): When `true`, the app should display maintenance state. -- `maintenance_message` (string): Message shown during maintenance. - ---- - -### default_environment - -Specifies the default environment for the application. Common values: - -- `sandbox` -- `production` -- `custom` - ---- - -### branding - -- `app_name` (string): App name displayed in UI. -- `show_logo` (boolean): Whether to show the logo. -- `logo_url` (string): Logo URL (absolute or static asset). -- `primary_color` (string): Primary color hex (e.g. `#1976D2`). -- `secondary_color` (string): Secondary color hex (e.g. `#424242`). - ---- - -### main_screen.locale - -- `title` (string): Main screen title. Supports `\n` for line breaks. -- `footer.privacy_policy` (string): Privacy policy label. -- `footer.privacy_policy_url` (string): Privacy policy URL. - ---- - -### pnv_screen.locale - -- `title` (string): Screen title. -- `description` (string): Screen description. -- `country_code_label` (string): Label for country code input. -- `phone_number_label` (string): Label for phone number input. -- `footer.privacy_policy` (string): Privacy policy label. -- `footer.privacy_policy_url` (string): Privacy policy URL. -- `errors.phone_required` (string): Missing phone error text. -- `errors.phone_invalid` (string): Invalid phone error text. -- `errors.country_code_required` (string): Missing country code error text. - ---- - -### ussd_screen - -#### `ussd_screen.locale` - -- `title` (string): Screen title. -- `description` (string): Screen description. -- `phone_number_label` (string): Label for phone input. -- `ussd_code_label` (string): Label for USSD code input. -- `verify_button` (string): Button label. -- `footer.privacy_policy` (string): Privacy policy label. -- `footer.privacy_policy_url` (string): Privacy policy URL. -- `errors.phone_required` (string): Missing phone error text. -- `errors.permission_required` (string): Missing permission error text. - -#### `ussd_screen.config` - -- `enabled` (boolean): Enables the USSD verification flow. -- `ussd_code` (string): USSD code to dial (e.g. `*123#`). -- `backend_url` (string): Backend URL used for USSD verification. -- `timeout_ms` (number): Timeout in milliseconds for USSD flow. - ---- - -### global_locale - -- `app_name` (string): Global app name shown in shared UI areas. -- `common` (object): Shared labels and button texts. - - `ok` (string) - - `cancel` (string) - - `back` (string) - - `info_title` (string) - ---- - -## Examples - -For practical examples of overriding app_config, see the example in [Load and Override Order](#load-and-override-order) section above. - ---- - -## Related Documentation - -- [Sample Config Reference](CONFIG_DEFAULT_JSON_SAMPLE.md): Complete configuration guide -- [Locale Configuration](LOCALE.md): Text label customization -- [API Documentation](API.md): API endpoint reference including `/api/config` diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md deleted file mode 100644 index ef55752..0000000 --- a/docs/ARCHITECTURE.md +++ /dev/null @@ -1,640 +0,0 @@ -# Architecture Overview - -Understanding how the IPification Showcase works at a high level. - -## Table of Contents - -- [What is this app?](#what-is-this-app) -- [How it works](#how-it-works) -- [Key Components](#key-components) -- [Authentication Flows](#authentication-flows) -- [Configuration](#configuration) -- [Security](#security) -- [Extending the App](#extending-the-app) -- [Technical Details](#technical-details) - ---- - -## What is this app? - -A **showcase/demo application** for IPification authentication services. It demonstrates: - -- ✅ Multiple authentication methods (IP, IM, SIM) -- ✅ Phone number verification flows -- ✅ QR code authentication for desktop -- ✅ Multi-server support (stage, live, etc.) - -**Tech stack**: Node.js + Express + Pug templates - ---- - -## How it works - -``` -┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ -│ Browser │────────│ This App │────────│ IPification API │ -│ │ │ (Express) │ │ (OAuth Server) │ -└─────────────┘ └──────────────┘ └─────────────────┘ - -Flow: -1. User clicks button -2. App redirects to IPification -3. User authenticates -4. IPification redirects back -5. App shows user info -``` - ---- - -## Key Components - -### Frontend - -- **Pug Templates**: Login page, user info page -- **JavaScript**: Button handlers, QR code, phone input -- **CSS**: Styling and animations - -### Backend - -- **Express Routes**: Handle auth flow -- **Session Management**: Track user state -- **OAuth2 Client**: Talk to IPification -- **WebSocket**: For QR code flow (desktop ↔ mobile) - -### Configuration - -- **auth_servers**: Which IPification servers to use -- **clients**: Which auth flows to enable -- **locale**: UI text customization - ---- - -## Authentication Flows - -### Standard Flow (Mobile/Desktop Web) - -For the IP phone-verification walkthrough, request examples, and source map, see [PNV via IP](PNV.md). - -``` -User clicks button - ↓ -/auth/start (redirects to IPification) - ↓ -User authenticates on IPification - ↓ -/auth/callback (receives code) - ↓ -Exchange code for user info - ↓ -Show user info page -``` - -### QR Code Flow (Desktop + Mobile) - -``` -Desktop: Generate QR code with auth URL -Mobile: Scan QR → authenticate -Desktop: Receives notification via WebSocket → shows result -``` - -### SIM Flow (TS43) - -``` -User clicks SIM button - ↓ -Browser requests SIM credential - ↓ -User confirms on device - ↓ -Show verified phone number -``` - ---- - -## Configuration - -Three main config files: - -1. **config/default.json** - Your setup (auth servers, clients, credentials) -2. **config/locale.json** - UI text labels -3. **config/app_config.json** - App behavior settings - -See [CONFIG_DEFAULT_JSON_SAMPLE.md](CONFIG_DEFAULT_JSON_SAMPLE.md) for details. - ---- - -## Security - -- ✅ OAuth2 authorization code flow -- State parameter for callback/result correlation; the standard web flow does not compare it against a saved session value (see [PNV limitations](PNV.md#verification-results-and-current-limitations)) -- ✅ Session management -- ✅ HTTPS recommended for production -- ✅ Helmet.js security headers - ---- - -## Extending the App - -### Add new authentication flow - -1. Add client config in `config/default.json` -2. Add button in `views/login.pug` -3. Done! Routes handle it automatically - -### Add new auth server - -1. Add to `auth_servers` array in config -2. Restart app -3. Dropdown appears automatically - -### Customize UI text - -1. Edit `config/locale.json` -2. Or override in `config/default.json` -3. Restart app - ---- - -## Technical Details - -
-📚 Click for detailed architecture diagrams and module structure - -### System Architecture - -### Component Architecture - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Express Application │ -├─────────────────────────────────────────────────────────────┤ -│ │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ Routes │ │ Middleware │ │ Views │ │ -│ │ │ │ │ │ (Pug) │ │ -│ │ - auth.js │ │ - Session │ │ - login.pug │ │ -│ │ - user.js │ │ - Helmet │ │ - info.pug │ │ -│ │ - ts43.js │ │ - Logger │ │ - qr_*.pug │ │ -│ │ │ │ - Parser │ │ │ │ -│ └──────────────┘ └──────────────┘ └──────────────┘ │ -│ │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ Services │ │ Libraries │ │ Socket.io │ │ -│ │ │ │ │ │ │ │ -│ │ - DataStore │ │ - Axios │ │ - Real-time │ │ -│ │ │ │ - GeoIP │ │ Events │ │ -│ └──────────────┘ └──────────────┘ └──────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────┘ -``` - ---- - -## Application Flow - -### Standard Authentication Flow - -``` -┌─────────┐ ┌──────────┐ ┌──────────────┐ ┌──────────┐ -│ Browser │ │ Express │ │ IPification │ │ Browser │ -└────┬────┘ └────┬─────┘ └──────┬───────┘ └────┬─────┘ - │ │ │ │ - │ GET /auth/start│ │ │ - ├───────────────>│ │ │ - │ │ Build Auth URL │ │ - │ │──────────────────>│ │ - │ │ │ │ - │ 302 Redirect │ │ │ - │<───────────────┤ │ │ - │ │ │ │ - │ │ │ User Auth │ - │ │ │<─────────────────┤ - │ │ │ │ - │ │ Callback │ │ - │ │<──────────────────┤ │ - │ │ │ │ - │ │ Exchange Code │ │ - │ │──────────────────>│ │ - │ │ │ │ - │ │ Access Token │ │ - │ │<──────────────────┤ │ - │ │ │ │ - │ │ Get User Info │ │ - │ │──────────────────>│ │ - │ │ │ │ - │ │ User Info │ │ - │ │<──────────────────┤ │ - │ │ │ │ - │ 302 Redirect │ │ │ - │<───────────────┤ │ │ - │ │ │ │ - │ GET /auth/complete │ │ - ├───────────────>│ │ │ - │ │ │ │ - │ 302 Redirect │ │ │ - │<───────────────┤ │ │ - │ │ │ │ - │ GET /user/info │ │ │ - ├───────────────>│ │ │ - │ │ │ │ - │ HTML Response │ │ │ - │<───────────────┤ │ │ -``` - -### QR Code Flow (Desktop) - -``` -┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ -│ Desktop │ │ Express │ │ Mobile │ │Express │ -│ Browser │ │ Server │ │ Device │ │ Socket │ -└────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ - │ │ │ │ - │ GET /auth/start │ │ │ - ├───────────────>│ │ │ - │ │ │ │ - │ QR Code Display │ │ │ - │<───────────────┤ │ │ - │ │ │ │ - │ Socket: init │ │ │ - ├───────────────>│ │ │ - │ │ Join Room │ │ - │ │────────────────>│ │ - │ │ │ │ - │ │ │ Scan QR & Auth │ - │ │ │────────────────>│ - │ │ │ │ - │ │ Callback │ │ - │ │<─────────────────────────────────┤ - │ │ │ │ - │ │ Socket: url │ │ - │ │────────────────>│ │ - │ │ │ │ - │ Socket Event │ │ │ - │<───────────────┤ │ │ - │ │ │ │ - │ Navigate to URL │ │ │ - ├───────────────>│ │ │ - │ │ │ │ - │ User Info │ │ │ - │<───────────────┤ │ │ -``` - -## Data Flow - -### Session Management - -``` -Request → Session Middleware → Session Store → Request Object - │ - └─> res.locals (template variables) -``` - -### State Management - -``` -Browser → Generate State → /auth/start → Include in Auth URL - │ - └─> IPification - │ -Callback ←────────────────────────────────────────────────┘ - │ - └─> Store User Info with State Key - │ -Complete ← Retrieve User Info ← Data Store -``` - -### Data Store Structure - -``` -Key Format: Value: -───────────────────────────────────────────────────────────── -{state} { userInfo, client_id, ... } -{state}-user-info { raw user info object } -``` - ---- - -## Security Architecture - -### Security Layers - -1. **Helmet.js**: HTTP security headers -2. **Session Management**: Secure session cookies -3. **State Parameter**: Callback/result correlation; no standard web session-state comparison -4. **Input Validation**: Parameter validation -5. **Secret Keys**: Environment-based secrets - -### Authentication Flow Security - -``` -┌─────────────────────────────────────────────────────────┐ -│ Security Measures │ -├─────────────────────────────────────────────────────────┤ -│ │ -│ 1. State Parameter (Result Correlation) │ -│ - Generated on client │ -│ - Not compared against browser-session state │ -│ │ -│ 2. OAuth2 Authorization Code Flow │ -│ - Short-lived authorization codes │ -│ - Server-side token exchange │ -│ │ -│ 3. Session Management │ -│ - HttpOnly cookies │ -│ - Secure flag (HTTPS) │ -│ │ -│ 4. Secret Management │ -│ - Environment variables │ -│ - Git-ignored config files │ -│ │ -└─────────────────────────────────────────────────────────┘ -``` - ---- - -## Module Structure - -### Application Setup - -`app.js` remains the application assembly point: it validates configuration, configures shared middleware and views, and explicitly registers the route prefixes. - -| Module | Responsibility | -| --- | --- | -| `middleware/appLocals.js` | Create request locals with inherited client credentials, locale, base URL, and server/flow lookup helpers | -| `bootstrap/playIntegrity.js` | Initialize and mount Play Integrity only when `PLAY_INTEGRITY_ENABLED=true`, with the configured verification and attempt services | - -Request locals run before the feature and general routes. Play Integrity is mounted before the general body parsers so its own 16 KB JSON parser and safe payload error responses take precedence. The TS43 auth rate limiter remains immediately before the TS43 router, and the application error handler remains last. - -### Route Modules - -#### `routes/auth.js` - -- **Purpose**: Register authentication endpoints; delegate HTTP handling to the web and API modules below. -- **Key Functions**: - - `GET /auth/login`: Render login page - - `GET /auth/start`: Initiate OAuth flow - - `GET /auth/callback/:userFlow/:serverId`: Handle OAuth callback - - `GET /auth/complete`: Complete authentication - - `GET /auth/qrcode/complete`: Render QR success page - - `GET /auth/qrcode/error`: Render QR error page - - `POST /auth/mobile/login`: Mobile login endpoint - - `POST /auth/s2s/signin`: Server-to-server sign-in - -#### Authentication handlers and services - -| Module | Responsibility | -| --- | --- | -| `routes/auth/webHandlers.js` | Login/start/callback/completion handlers, page rendering, session updates, and QR socket notifications | -| `routes/auth/apiHandlers.js` | Mobile login and S2S sign-in HTTP responses | -| `services/authService.js` | Build authorization URLs and exchange authorization codes through `utils/httpClient.js`; no Express or cache access | -| `services/authResultStore.js` | Save formatted results and raw userinfo, read S2S userinfo, and poll for completion results using the existing memory cache | - -The callback exchanges the provider code and fetches userinfo before saving the formatted response under `state` and raw userinfo under `${state}-user-info`. After both writes complete, a backchannel request returns an empty 200 response. Otherwise, QR callbacks notify the desktop through Socket.IO and render the mobile result through a redirect; ordinary web callbacks redirect to `/auth/complete`. The desktop also uses `/auth/complete` to adopt the stored result into its browser session and continue to `/user/info`. - -Completion polls the result cache up to five times, waiting one second after each miss, including the final miss. Reads retain the cached entries. Mobile login exchanges the code directly and returns userinfo without using this completion cache. - -#### `routes/user.js` - -- **Purpose**: User information display -- **Key Functions**: - - `GET /user/info`: Display authenticated user info - -#### `routes/ts43.js` - -- **Purpose**: Register the TS43 endpoints and delegate to `routes/ts43/handlers.js`. -- **Key Functions**: - - `POST /ts43/auth`: Initiate TS43 authentication - - `POST /ts43/token`: Exchange VP token - - `POST /ts43/log`: Logging endpoint - -#### `routes/sms.js` - -- **Purpose**: Register the SMS endpoints and delegate to `routes/sms/handlers.js`. -- **Key Functions**: - - `POST /sms/auth`: Initiate SMS authentication - - `POST /sms/token`: Submit the SMS code and exchange the CIBA request for userinfo - - `POST /sms/log`: Logging endpoint - -#### TS43 and SMS handlers and services - -| Module | Responsibility | -| --- | --- | -| `routes/ts43/handlers.js`, `routes/sms/handlers.js` | Resolve server/client configuration, handle requests and responses, format display data, and update browser sessions | -| `services/ts43Service.js` | Start CIBA authentication, fetch DCQL data and build the digital request, then submit the VP callback and exchange for token/userinfo | -| `services/smsService.js` | Start CIBA SMS authentication, then submit the SMS callback and exchange for token/userinfo | - -Both services expose `startAuthentication` and `exchangeToken` and operate without Express request/response objects. Provider callbacks are best-effort: a callback error is logged, then the token exchange still runs. The handlers retain the demo's existing session behavior: token/userinfo errors also populate an authenticated session with formatted error data. This extraction preserves those behaviors and the endpoint-specific error bodies; it does not introduce a shared CIBA abstraction. - -### Library Modules - -#### `lib/data_store.js` - -- **Purpose**: Temporary data storage -- **Implementation**: In-memory cache using `cache-manager` -- **TTL**: 60 minutes -- **Usage**: Store authentication state and user information - -### Utility Modules - -#### `utils/authServerHelper.js` - -- **Purpose**: Auth server lookup and validation utilities -- **Key Functions**: - - `getAuthServer(authServers, serverId)`: Get server by ID or default to first - - `getAuthServerIds(authServers)`: Get all available server IDs - - `hasAuthServer(authServers, serverId)`: Check if server exists - -#### `utils/validateConfig.js` - -- **Purpose**: Configuration validation on application startup -- **Key Functions**: - - `validateConfig(config)`: Main validation entry point - - `validateAuthServers(authServers)`: Validate auth servers array - - `validateClients(clients)`: Validate client configurations - - `validateRealm(realm)`: Validate realm configuration - - `validateCredentials(config)`: Validate OAuth credentials -- **Behavior**: Throws error and prevents app startup if validation fails - -#### `utils/helpers.js` - -- **Purpose**: General utility functions -- **Key Functions**: - - `deepMerge(target, source)`: Deep merge objects - - `findClientByUserFlow(clients, userFlow)`: Find client by flow ID - - `findClientByClientId(clients, clientId)`: Find client by client ID - -#### `utils/constants.js` - -- **Purpose**: Application constants -- **Contents**: HTTP status codes, error messages, retry settings - -### Socket Module - -#### `socket.js` - -- **Purpose**: WebSocket server for real-time communication -- **Implementation**: Socket.io -- **Channels**: Room-based (`auth:{state}`) -- **Events**: - - `init`: Client joins room - - `messages`: Server sends events - ---- - -## Configuration Management - -### Configuration Hierarchy - -``` -1. Environment Variables (.env) -2. config/default.json -3. Default values in code -``` - -### Configuration Validation - -On application startup, configuration is validated using `utils/validateConfig.js`: - -1. **Required Fields**: `realm`, `auth_servers`, `client_id`, `client_secret`, `clients` -2. **Auth Servers**: Must have at least one server with valid `id` and `url` -3. **Clients**: Must have unique `user_flow` values -4. **Behavior**: App crashes on startup if validation fails - -See [Sample Config Reference](CONFIG_DEFAULT_JSON_SAMPLE.md) for field descriptions. - -### Configuration Flow - -``` -app.js → require('config') → config/default.json - │ - └─> res.locals → Available in templates -``` - ---- - -## Error Handling - -### Error Flow - -``` -Error Occurred - │ - ├─> Try-Catch Block - │ │ - │ └─> Log Error - │ │ - │ └─> Error Handler Middleware - │ │ - │ └─> HTTP Error Response - │ - └─> OAuth Errors - │ - └─> Redirect to Login with Error Message -``` - ---- - -## Performance Considerations - -### Caching Strategy - -- **Data Store**: In-memory cache with 60-minute TTL -- **Session Store**: Memory-based (consider Redis for production) -- **Static Assets**: Express static middleware - -### Optimization Opportunities - -1. **Redis Integration**: Replace in-memory cache with Redis -2. **Session Store**: Use Redis session store for scalability -3. **Rate Limiting**: Implement rate limiting middleware -4. **Connection Pooling**: Configure HTTP client connection pooling - ---- - -## Deployment Architecture - -### Docker Deployment - -``` -┌─────────────────────────────────────────┐ -│ Docker Compose Stack │ -├─────────────────────────────────────────┤ -│ │ -│ ┌─────────────┐ ┌─────────────┐ │ -│ │ App │──────│ Redis │ │ -│ │ Container │ │ Container │ │ -│ │ Port: 8080 │ │ Port: 6379 │ │ -│ └─────────────┘ └─────────────┘ │ -│ │ -└─────────────────────────────────────────┘ -``` - -### Production Considerations - -1. **Environment Variables**: Use secrets management -2. **HTTPS**: Configure reverse proxy (nginx/traefik) -3. **Load Balancing**: Multiple app instances -4. **Monitoring**: Application performance monitoring -5. **Logging**: Centralized logging system - ---- - -## Extension Points - -### Adding New User Flows - -1. Add client configuration to `config/default.json` (see [Sample Config Reference](CONFIG_DEFAULT_JSON_SAMPLE.md)) -2. Add UI button in `views/login.pug` -3. Add JavaScript handler in `public/js/login.js` -4. Flow automatically handled by existing routes - -### Adding New Authentication Methods - -1. Extend `routes/auth.js` with new endpoint -2. Implement OAuth2/OIDC flow -3. Add UI components -4. Update documentation - -### Customizing User Info Display - -1. Modify `views/info.pug` template -2. Update `routes/user.js` if needed -3. Customize styling in `public/css/` - ---- - -## Dependencies - -### Core Dependencies - -- **express**: Web framework -- **express-session**: Session management -- **socket.io**: WebSocket server -- **axios**: HTTP client -- **pug**: Template engine -- **config**: Configuration management - -### Security Dependencies - -- **helmet**: Security headers -- **cookie-parser**: Cookie parsing - -### Utility Dependencies - -- **uuid**: UUID generation -- **geoip-lite**: GeoIP lookup -- **cache-manager**: Caching abstraction - ---- - -## Future Enhancements - -1. **Redis Integration**: Replace in-memory cache -2. **Rate Limiting**: Add rate limiting middleware -3. **API Versioning**: Implement API versioning -4. **OpenAPI Documentation**: Generate OpenAPI specs -5. **Testing**: Add unit and integration tests -6. **Monitoring**: Add application monitoring -7. **Logging**: Structured logging with correlation IDs - -
diff --git a/docs/CONFIG_DEFAULT_JSON_SAMPLE.md b/docs/CONFIG_DEFAULT_JSON_SAMPLE.md deleted file mode 100644 index 621115c..0000000 --- a/docs/CONFIG_DEFAULT_JSON_SAMPLE.md +++ /dev/null @@ -1,255 +0,0 @@ -# default.json.sample configuration - -This document describes every field in `config/default.json.sample` and how to use it. - -## Table of Contents - -- [Quick Reference](#quick-reference) -- [How to use](#how-to-use) -- [Required Configuration](#required-configuration) -- [Optional Configuration](#optional-configuration) -- [auth_servers[] fields](#auth_servers-fields) -- [clients[] fields](#clients-fields) -- [Available User Flows](#available-user-flows) -- [locale fields](#locale-fields) -- [Troubleshooting](#troubleshooting) - -## Quick Reference - -**Minimum required configuration to start the app:** - -```json -{ - "realm": "ipification", - "auth_servers": [ - { - "id": "stage", - "url": "https://api.stage.ipification.com/auth" - } - ], - "client_id": "your_client_id", - "client_secret": "your_client_secret", - "clients": [ - { - "user_flow": "pvn_ip", - "title": "IP Phone Number Verification", - "scope": "openid ip:phone_verify" - } - ] -} -``` - -## How to use - -1. Copy `config/default.json.sample` to `config/default.json`. -2. Fill in the values for your environment (especially `auth_servers` and client credentials). -3. Restart the app so the config is reloaded. - -**⚠️ Important:** The `realm`, `auth_servers`, `client_id`, `client_secret`, and `clients` fields are **required**. The app will fail to start if these are not properly configured. - -## Field reference - -### Required Configuration - -These fields **must** be configured for the app to start properly. The app will fail validation and crash on startup if any of these are missing or invalid. - -- `realm` (string): Keycloak realm used in auth URLs (for example, `ipification`). - -- `auth_servers` (array): List of authentication server configurations. Must contain at least one server. See [auth_servers[] fields](#auth_servers-fields) for details. - -- `client_id` (string): Default OAuth client ID used as a fallback for all flows in the `clients` array. - - Individual flows can override this by specifying their own `client_id`. - - If a flow doesn't specify `client_id`, this root-level value is used. - - **Recommended**: Use one client for all flows unless you have specific requirements. - -- `client_secret` (string): Default OAuth client secret used as a fallback for all flows in the `clients` array. - - Individual flows can override this by specifying their own `client_secret`. - - If a flow doesn't specify `client_secret`, this root-level value is used. - - **Recommended**: Use one client secret for all flows unless you have specific requirements. - -- `clients` (array): List of client configurations, one per user flow. Each object describes a flow button and its OAuth client. See [clients[] fields](#clients-fields) for details. - -### Optional Configuration - -These fields are optional and have sensible defaults or can be left empty. - -- `title` (string): Page title shown in the browser tab and some templates. Default: `"IPification Showcase"`. - -- `app_logo_url` (string): Logo image URL. Can be a relative path in `public/` (like `/images/logo/vil.svg`) or an absolute URL. If not specified, only the app name is shown. - -- `app_logo_style` (string): Inline CSS applied to the logo image in the login view (e.g. `width: 50px; height: 50px;`). Only applies if `app_logo_url` is set. - -- `favicon_url` (string): URL to a favicon image (relative or absolute). If not specified, uses the default favicon at `/images/logo.ico`. - -- `default_country_code` (string): Two-letter country code used when the phone country is not detected (e.g. `vn`). Default: `"rs"`. - -- `disabled_select_country` (boolean): When `true`, disables the country selector UI and forces the default country. Default: `false`. - -- `locale` (object): Optional overrides for UI text. See `docs/LOCALE.md` for full localization options. If not specified, uses default English locale. - ---- - -### auth_servers[] fields - -Each entry defines an authentication server that users can select from. - -- `id` (string, **required**): Unique identifier for the auth server (e.g., `stage`, `live`, `prod`). -- `url` (string, **required**): Full base URL of the authentication server (e.g., `https://api.stage.ipification.com/auth`). Must be a valid URL format. - -**Multiple Server Behavior:** - -- If **1 server** is configured: The app uses it automatically without showing a dropdown. -- If **2+ servers** are configured: A dropdown selector appears on the login page allowing users to choose between servers. The selected server is saved to localStorage and persists across page reloads. - -**Example configuration:** - -```json -{ - "auth_servers": [ - { - "id": "stage", - "url": "https://api.stage.ipification.com/auth" - }, - { - "id": "live", - "url": "https://api.ipification.com/auth" - } - ] -} -``` - -**Validation:** - -- The `id` field must be unique across all servers -- The `url` field must be a valid URL format -- At least one server must be configured -- The app will crash on startup if validation fails - ---- - -### clients[] fields - -Each entry defines a user flow and its OAuth client credentials. - -- `user_flow` (string, **required**): Unique flow ID used in routes and buttons (e.g. `pvn_ip`, `login_ip`, `kyc_phone`). Must be unique across all clients. - -- `client_id` (string, optional): OAuth client ID for this specific flow. - - **If specified**: Overrides the root-level `client_id` for this flow only. - - **If not specified**: Uses the root-level `client_id` as fallback. - - **Use case**: When different flows need different OAuth clients. - -- `client_secret` (string, optional): OAuth client secret for this specific flow. - - **If specified**: Overrides the root-level `client_secret` for this flow only. - - **If not specified**: Uses the root-level `client_secret` as fallback. - - **Use case**: When different flows need different OAuth clients. - -- `title` (string, **required**): Display label for the flow button on the login page. - -- `scope` (string, **required**): Space-separated scopes requested for the flow (for example, `openid ip:phone_verify`). - -- `channel` (string, optional): Channel list for IM flows (space-separated, e.g. `wa viber telegram`). Only used for IM flows (`pvn_im`, `login_im`). - -**Example: Using root-level credentials (recommended for most cases):** - -```json -{ - "client_id": "default_client", - "client_secret": "default_secret", - "clients": [ - { - "user_flow": "pvn_ip", - "title": "IP Verification", - "scope": "openid ip:phone_verify" - // Uses client_id: "default_client" and client_secret: "default_secret" - }, - { - "user_flow": "login_ip", - "title": "IP Login", - "scope": "openid ip:phone" - // Uses client_id: "default_client" and client_secret: "default_secret" - } - ] -} -``` - -**Example: Overriding credentials per flow:** - -```json -{ - "client_id": "default_client", - "client_secret": "default_secret", - "clients": [ - { - "user_flow": "pvn_ip", - "title": "IP Verification", - "scope": "openid ip:phone_verify" - // Uses root-level: "default_client" and "default_secret" - }, - { - "user_flow": "pvn_im", - "client_id": "im_client", - "client_secret": "im_secret", - "title": "IM Verification", - "scope": "openid ip:phone_verify", - "channel": "wa viber telegram" - // Overrides with: "im_client" and "im_secret" - }, - { - "user_flow": "login_ip", - "client_id": "login_client", - "title": "IP Login", - "scope": "openid ip:phone" - // Overrides client_id but uses root-level client_secret - } - ] -} -``` - ---- - -### Available User Flows - -| User Flow | Description | Scope | -| -------------------------- | ------------------------------ | ------------------------------------- | -| `pvn_ip` | IP Phone Number Verification | `openid ip:phone_verify` | -| `pvn_ip_plus` | IP+ Phone Number Verification | `openid ip:phone_verify ip:mobile_id` | -| `pvn_im` | IM Phone Number Verification | `openid ip:phone_verify` | -| `pvn_ipificator` | IP via App Verification | `openid ip:phone_verify` | -| `pvn_sim` | SIM Phone Number Verification | `openid ip:phone_verify` | -| `login_ip` | IP Login | `openid ip:phone` | -| `login_ip_plus` | IP+ Login | `openid ip:mobile_id ip:phone` | -| `login_im` | IM Login | `openid ip:phone` | -| `login_sim` | SIM Login | `openid ip:phone` | -| `anonymous` | Anonymous Identity | `openid ip:mobile_id` | -| `kyc_phone` | KYC Data | `openid ip:profile` | -| `sample_app_im_backchannel`| Sample IM Backchannel | `openid` | - ---- - -### locale fields - -- `locale.app.name` (string): Overrides the application name used in the UI. -- `locale.tabs.pnv` (string): Label for the Phone Number Verification tab. -- `locale.tabs.login` (string): Label for the Login tab. - ---- - -## Troubleshooting - -### Configuration Errors - -**Error: "Configuration error: auth_servers must be defined as an array in config"** - -- Ensure `auth_servers` is present in your `default.json` and is an array. - -**Error: "Configuration error: auth_servers must contain at least one server"** - -- Add at least one server to the `auth_servers` array. - -**Error: "Configuration error: auth_servers[0] must have an 'id' (string)"** - -- Each server must have an `id` field with a string value. - -**Error: "Configuration error: Duplicate auth server IDs found: stage"** - -- Each server's `id` must be unique. Remove or rename duplicate IDs. diff --git a/docs/INDEX.md b/docs/INDEX.md index fec2ce9..44655f3 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -1,327 +1,5 @@ -# Documentation Index +# Documentation -Welcome! Find what you need quickly. +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. -## 🚀 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/LOCALE.md b/docs/LOCALE.md deleted file mode 100644 index f1ac150..0000000 --- a/docs/LOCALE.md +++ /dev/null @@ -1,431 +0,0 @@ -# Locale Configuration - -This document explains how to configure and override locale strings (text labels) in the IPification Showcase application. - -## Table of Contents - -- [Overview](#overview) -- [Locale File Structure](#locale-file-structure) -- [Default Locale File](#default-locale-file) -- [Overriding Locale Strings](#overriding-locale-strings) -- [Deep Merging Behavior](#deep-merging-behavior) -- [Using Locale in Views](#using-locale-in-views) -- [Locale Structure Reference](#locale-structure-reference) -- [Best Practices](#best-practices) -- [Troubleshooting](#troubleshooting) -- [Implementation Details](#implementation-details) -- [Related Documentation](#related-documentation) -- [Examples](#examples) - ---- - -## Overview - -The application uses a locale system that allows you to customize all text labels displayed in the user interface. Locale strings are stored in `config/locale.json` and can be overridden in `config/default.json` for environment-specific customizations. For config field details, see [Sample Config Reference](CONFIG_DEFAULT_JSON_SAMPLE.md). - ---- - -## Locale File Structure - -The locale system is organized into logical sections: - -```json -{ - "app": { - "name": "Application name displayed in header" - }, - "tabs": { - "pnv": "PNV tab label", - "login": "Login tab label", - "identity": "Identity tab label" - }, - "sections": { - "pnv": { - "title": "Section title", - "description": "Section description text", - "button_ip_label": "Optional: Override IP button label for PNV", - "button_ip_plus_label": "Optional: Override IP+ button label for PNV", - "button_im_label": "Optional: Override IM button label for PNV", - "button_sim_label": "Optional: Override SIM button label for PNV", - "button_ip_via_app_label": "Optional: Override IP via App button label for PNV" - }, - "login": { - "title": "Section title", - "description": "Section description text", - "button_ip_label": "Optional: Override IP button label for Login", - "button_ip_plus_label": "Optional: Override IP+ button label for Login", - "button_im_label": "Optional: Override IM button label for Login", - "button_sim_label": "Optional: Override SIM button label for Login" - }, - "identity": { - "title": "Section title", - "description": "Section description text", - "button_anonymous_identity_label": "Optional: Override Anonymous Identity button label", - "button_kyc_label": "Optional: Override KYC button label" - } - }, - "buttons": { - "ip": "IP button label", - "ip_plus": "IP+ button label", - "im": "IM button label", - "sim": "SIM button label", - "ip_via_app": "IP via App button label", - "anonymous_identity": "Anonymous Identity button label", - "kyc": "KYC button label" - }, - "footer": { - "viettel_legal": "Legal disclaimer text", - "viettel": "Viettel brand name", - "privacy_policy": "Privacy Policy link text", - "about": "About link text", - "help": "Help link text" - }, - "modals": { - "about_title": "About modal title", - "server_label": "Server label", - "state_label": "State label", - "developer_link": "Developer link text", - "phone_required": "Phone number required error message", - "phone_invalid": "Invalid phone number error message" - } -} -``` - ---- - -## Default Locale File - -The default locale strings are stored in `config/locale.json`. This file contains all the standard English text labels used throughout the application. - -**Location**: `config/locale.json` - -**Purpose**: Base locale strings that are used by default. - ---- - -## Overriding Locale Strings - -You can override any locale string by adding a `locale` section to your `config/default.json` file. See [Sample Config Reference](CONFIG_DEFAULT_JSON_SAMPLE.md) for the full config layout. The override system uses deep merging, which means: - -- You can override individual nested properties without replacing entire sections -- Only the properties you specify will be overridden -- All other properties will remain from the default locale file - -### How Override Works - -1. The application loads `config/locale.json` as the base locale -2. If `config/default.json` contains a `locale` section, it merges the override values -3. The merged locale is made available to all views via `res.locals.locale` - -### Override Examples - -#### Example 1: Override Application Name - -```json -{ - "realm": "ipification", - "auth_servers": [...], - "clients": [...], - "locale": { - "app": { - "name": "My Custom IPification App" - } - } -} -``` - -#### Example 2: Override Tab Labels - -```json -{ - "locale": { - "tabs": { - "pnv": "Phone Verification", - "login": "User Login", - "identity": "User Identity" - } - } -} -``` - -#### Example 3: Override Section Titles and Descriptions - -```json -{ - "locale": { - "sections": { - "pnv": { - "title": "Verify Your Phone Number", - "description": "Use IP or IM authentication to verify your phone number." - }, - "login": { - "title": "Quick Login", - "description": "Sign in using IP or IM authentication with automatic phone number sharing." - } - } - } -} -``` - -#### Example 4: Override Button Labels - -```json -{ - "locale": { - "buttons": { - "ip": "Internet Protocol", - "ip_plus": "IP Plus", - "im": "Instant Messaging", - "kyc": "Know Your Customer" - } - } -} -``` - -#### Example 5: Override Multiple Sections - -```json -{ - "locale": { - "app": { - "name": "Custom Brand Name" - }, - "tabs": { - "pnv": "Verification", - "login": "Access" - }, - "sections": { - "pnv": { - "title": "Phone Verification", - "description": "Verify your phone number" - } - }, - "buttons": { - "ip": "IP Auth", - "im": "IM Auth" - }, - "modals": { - "phone_required": "Please provide your phone number", - "phone_invalid": "The phone number format is incorrect" - } - } -} -``` - ---- - -## Deep Merging Behavior - -The locale override system uses deep merging, which means: - -- **Nested objects are merged**: If you override `app.name`, the entire `app` object is not replaced, only `name` is updated -- **Partial overrides work**: You can override just `sections.pnv.title` without affecting `sections.pnv.description` -- **Arrays are replaced**: If a locale property is an array, the entire array is replaced (not merged) - -### Deep Merge Example - -**Base locale.json:** - -```json -{ - "sections": { - "pnv": { - "title": "Phone Number Verification", - "description": "Verify phone number through IP or IM auth." - } - } -} -``` - -**Override in default.json:** - -```json -{ - "locale": { - "sections": { - "pnv": { - "title": "Custom Title" - } - } - } -} -``` - -**Result:** - -```json -{ - "sections": { - "pnv": { - "title": "Custom Title", - "description": "Verify phone number through IP or IM auth." - } - } -} -``` - -Notice that `description` is preserved from the base locale. - -## Using Locale in Views - -Locale strings are automatically available in all Pug templates via the `locale` variable. Access nested properties using dot notation: - -```pug -h3.app-name #{locale.app.name} -p.description_title #{locale.sections.pnv.title} -button #{locale.buttons.ip} -``` - -### HTML Support in Locale Strings - -Some locale fields support HTML tags for formatting. Use `!{}` instead of `#{}` to render HTML: - -**Escaped (default)** - Use `#{}` for plain text: - -```pug -p.description #{locale.sections.pnv.title} -``` - -**Unescaped (HTML)** - Use `!{}` for HTML content: - -```pug -p.description !{locale.sections.pnv.description} -``` - -**Supported HTML tags:** - -- `
` or `
` - Line breaks -- ``, `` - Bold text -- ``, `` - Italic text -- `` - Links -- Other HTML tags as needed - -**Example with HTML:** - -```json -{ - "sections": { - "pnv": { - "title": "Phone Number Verification", - "description": "Basic - verify your phone number
Advance - verify your phone number with fraud prevention signals" - } - } -} -``` - -**Note:** Fields that support HTML are typically description fields. Title and button labels should remain plain text for consistency. - -## Locale Structure Reference - -### App Section - -- `app.name` - Application name displayed in the header - -### Tabs Section - -- `tabs.pnv` - PNV tab label -- `tabs.login` - Login tab label -- `tabs.identity` - Identity tab label - -### Sections Section - -#### PNV Section - -- `sections.pnv.title` - PNV section title -- `sections.pnv.description` - PNV section description -- `sections.pnv.button_ip_label` - Optional: Override IP button label for PNV section -- `sections.pnv.button_ip_plus_label` - Optional: Override IP+ button label for PNV section -- `sections.pnv.button_im_label` - Optional: Override IM button label for PNV section -- `sections.pnv.button_sim_label` - Optional: Override SIM button label for PNV section -- `sections.pnv.button_ip_via_app_label` - Optional: Override IP via App button label for PNV section - -#### Login Section - -- `sections.login.title` - Login section title -- `sections.login.description` - Login section description -- `sections.login.button_ip_label` - Optional: Override IP button label for Login section -- `sections.login.button_ip_plus_label` - Optional: Override IP+ button label for Login section -- `sections.login.button_im_label` - Optional: Override IM button label for Login section -- `sections.login.button_sim_label` - Optional: Override SIM button label for Login section - -#### Identity Section - -- `sections.identity.title` - Identity section title -- `sections.identity.description` - Identity section description -- `sections.identity.button_anonymous_identity_label` - Optional: Override Anonymous Identity button label -- `sections.identity.button_kyc_label` - Optional: Override KYC button label - -### Buttons Section - -- `buttons.ip` - IP button label -- `buttons.ip_plus` - IP+ button label -- `buttons.im` - IM button label -- `buttons.sim` - SIM button label -- `buttons.ip_via_app` - IP via App button label -- `buttons.anonymous_identity` - Anonymous Identity button label -- `buttons.kyc` - KYC button label - -### Footer Section - -- `footer.viettel_legal` - Legal disclaimer text -- `footer.viettel` - Viettel brand name -- `footer.privacy_policy` - Privacy Policy link text -- `footer.about` - About link text -- `footer.help` - Help link text - -### Modals Section - -- `modals.about_title` - About modal title -- `modals.server_label` - Server label in about modal -- `modals.state_label` - State label in about modal -- `modals.developer_link` - Developer link text -- `modals.phone_required` - Phone number required error message -- `modals.phone_invalid` - Invalid phone number error message - -## Best Practices - -1. **Keep base locale complete**: Always maintain a complete `locale.json` with all required strings -2. **Use overrides sparingly**: Only override strings that need to be different in specific environments -3. **Test overrides**: Verify that overrides work correctly and don't break the UI -4. **Document custom overrides**: If you add custom locale strings, document them in your project documentation -5. **Maintain consistency**: Keep naming conventions consistent across locale strings - -## Troubleshooting - -### Locale strings not updating - -- **Check file location**: Ensure `locale.json` is in `config/` directory -- **Verify JSON syntax**: Use a JSON validator to check for syntax errors -- **Check override syntax**: Ensure override in `default.json` uses correct nested structure -- **Restart application**: Changes to locale files require application restart - -### Override not working - -- **Verify deep merge**: Check that you're using nested object structure correctly -- **Check property names**: Ensure property names match exactly (case-sensitive) -- **Validate JSON**: Ensure both `locale.json` and `default.json` are valid JSON - -### Missing locale strings - -- **Check base locale**: Ensure `locale.json` contains all required strings -- **Verify view usage**: Check that views are accessing locale properties correctly -- **Check for typos**: Verify property names match between locale file and view - -## Implementation Details - -The locale system is implemented in `app.js`: - -1. **Loading**: `locale.json` is loaded at application startup -2. **Merging**: If `config.locale` exists, it's deep-merged with the base locale -3. **Availability**: Merged locale is added to `res.locals.locale` for all requests -4. **Deep merge**: Custom deep merge function handles nested object merging - -## Related Documentation - -- [Sample Config Reference](CONFIG_DEFAULT_JSON_SAMPLE.md) - Field-by-field config guide -- [Architecture Documentation](ARCHITECTURE.md) - System architecture details -- [Quick Start Guide](QUICKSTART.md) - Setup and configuration guide - -## Examples - -See [Sample Config Reference](CONFIG_DEFAULT_JSON_SAMPLE.md) for example locale override configurations. 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 new file mode 100644 index 0000000..d6ad653 --- /dev/null +++ b/docs/NEXTJS_BACKEND.md @@ -0,0 +1,137 @@ +# Next.js backend structure + +> 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 + +1. `src/app/**/route.ts` selects the endpoint. `auth/web.ts`, `auth/mobile.ts`, `ciba/handlers.ts` and `integrity/handlers.ts` are HTTP adapters: they parse requests and apply status codes, headers, cookies, redirects and safe error presentation. +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 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 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 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. 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 + +`tests/services/` injects repositories/providers and tests ordering, callback precedence, error/session adoption, domain policy and exchange completion without starting Next, Redis or a real provider. `tests/unit/provider-error.test.ts` verifies the normalized error envelope and safe callback classification. Existing HTTP contracts and Redis/process/Chromium suites exercise the production adapters. + +For this refactor, HTTP/process/browser checks run from a source-identical verification copy under the plan's ignored scratch directory, with private configs and env files excluded. Only the copied Turbopack root is adjusted so the dependency symlink stays inside its root. The developer's active server, `.next` output, private config and Redis are not stopped or reused. This verifies the copied current source, not staging or real-device/provider acceptance. + +The underlying Google verifier and Redis attempt implementation still contain legacy dynamic record handling; their application-facing ports are typed. Replacing those internals or tightening provider schemas is not implied by this refactor. + +## Acceptance record (2026-10-29) + +- Node 24.22.0: lint/typecheck and production build passed. +- Complete Next unit/HTTP run: 38 tests passed; one subsequently added attempt-creation case also passed in the five-test Integrity service file (39 distinct current tests verified). +- Redis/process integration: 8 passed, including cross-process callback/session sharing, restart, outage and concurrent Integrity exchanges. The optional Docker image test was skipped; no image rebuild/deployment claim is made for this refactor. +- Chromium: all 15 current scenarios passed on isolated port 3003. Existing uncommitted UI/test changes were included in the verification copy but not committed by this backend work. +- Legacy: 109 passed with a synthetic local-only config fixture. The initial copy lacked ignored config and failed one mounted-app test; rerunning with the fixture passed. No private credentials were used. +- 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. + +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 new file mode 100644 index 0000000..6008f69 --- /dev/null +++ b/docs/NEXTJS_MIGRATION.md @@ -0,0 +1,189 @@ +# 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 + + +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` | + +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 + + +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. 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. + +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 + +Run the legacy suite with Node 24.22.0 using `npm test`. Local provider stubs are not evidence of hosted or real-device success. + +## 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; 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 + +Use Node **24.22.0** and a disposable local Redis instance. Keep the legacy application on its original port; Next defaults to **3002**. + +```bash +cd showcase-next +npm ci +cp config/default.json.sample config/default.json +cp .env.example .env.local +# Fill in credentials in config/default.json and set REDIS_URL in .env.local. +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. 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. + +### Production build and Docker + +```bash +cd showcase-next +npm run build +npm start +``` + +The build requires no private JSON, Redis or provider credentials. Configure them at runtime before start. This application uses a custom server, not `next start` or Next standalone output. + +From the repository root: + +```bash +docker compose -f docker-compose.next.yml build +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. 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 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: + +```bash +docker compose -f docker-compose.next.yml up -d --force-recreate showcase-next +``` + +No image rebuild is needed. Recreate is important for single-file bind mounts after atomic host-file replacement. Play Integrity snapshots can retain prior credentials for their remaining 120-second lifetime. Google credentials and PLAY_INTEGRITY_* settings remain runtime-only and are not enabled in the sample Compose stack; add them through an operator-owned override and secret mount when needed. + +### Verification commands + +```bash +# Root: legacy characterization suite only +npm test + +# showcase-next/: unit/HTTP contracts, Redis/process integration, browser UI +npm run lint +npm run typecheck +npm test +npm run test:integration +npm run build +npx playwright install chromium +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. + +### 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: + +- Legacy suite: 109 passed. +- Next unit/HTTP contracts: 18 passed across 13 files. The parity fixture exercises all 21 existing method/path contracts, plus malformed/oversize JSON on eight general POST endpoints. This is fixture coverage, not exhaustive provider/input equivalence. +- Redis/process integration: 8 passed; the optional Docker test was run separately and passed (9 total). This includes callbacks across production processes, session/result reads after app restart, cross-process Integrity claim/exchange races, Redis outage, fixed TTL storage, Socket.IO polling/WebSocket room delivery, and shared rate counters. +- Chromium: 13 passed, including two-browser QR, SMS retries/cancellation, WebOTP cancellation, TS43 credential stubs, storage failures and mobile layout. +- ESLint, TypeScript, production build and final Docker image build passed. Docker smoke verified non-root execution, no baked private JSON, and runtime configuration changes after container recreation. + +Regression fixes from the final review pass: + +- Untrusted forwarded headers are removed before Next creates its request URL; `TRUST_PROXY_HOPS=0` now ignores a spoofed protocol as well as forwarded hostname/IP. Actual HTTP tests cover trust off/on, including a forged internal peer-IP header. +- General malformed/oversize request bodies retain legacy HTTP 500 (the Express error middleware maps parser errors to 500). The new response is safe JSON rather than a framework error page. Integrity keeps its separate HTTP 400 safe-error contract. + +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 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, 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. + +### Callback error screens + +Browser callback failures now redirect to `/auth/error` instead of returning the raw Axios message. The page explains the problem, gives recovery instructions, and includes an expandable diagnostic section plus a support reference. `invalid_client`/`unauthorized_client` indicate client authentication failure; `invalid_grant` indicates a rejected code (possibly expiry/reuse or client/redirect mismatch). An unqualified token 401 remains `TOKEN_REJECTED`: it does not prove a bad secret. Userinfo failures and unreachable/5xx providers have separate messages. + +The callback logs one `auth_callback_failed` JSON event containing the reference, classification, step, upstream HTTP status, and an allowlisted OAuth error code. It does not log Axios config, provider response descriptions, codes, credentials or tokens. Browser redirects include only these safe diagnostic fields. Unexpected query values are ignored by the rendered diagnostic section. Do not add raw provider descriptions to this page or URL. + +Backchannel failures remain HTTP 400 and now return safe JSON `{ error, reference }`; successful callbacks/backchannels and session completion are unchanged. Existing login error-query handling is unchanged. QR failure screens apply to the device opening the callback; an already-open desktop QR dialog still waits for successful completion. + +Verification: production build, lint/typecheck, 14 unit/HTTP tests (including existing unit tests), and all 14 Chromium scenarios passed. The new contract fixture covers five provider failure classes, backchannel failure, redaction and successful callback/session completion using disposable Redis and a local provider. No real-provider code was replayed. Use `E2E_PORT=3003 npm run test:e2e` to test while a developer server is using port 3002. + +### Showcase tab presentation + +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 a0add35..1bb8ca3 100644 --- a/docs/PLAY_INTEGRITY.md +++ b/docs/PLAY_INTEGRITY.md @@ -1,5 +1,10 @@ # 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. + + This demo verifies an Android Play Integrity token on the server at `POST /api/play-integrity/verify`. The endpoint is disabled unless `PLAY_INTEGRITY_ENABLED=true`. 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/PNV.md b/docs/PNV.md deleted file mode 100644 index 6ae2284..0000000 --- a/docs/PNV.md +++ /dev/null @@ -1,303 +0,0 @@ -# Phone Number Verification (PNV) via IP - -This guide follows the implemented IP verification flow: enter a phone number, redirect the browser to IPification, receive an authorization code, fetch userinfo, and display the result in a browser session. - -The UI calls this feature **PNV**, while its configured flow identifier is **`pvn_ip`**. Keep that identifier when configuring clients or calling the routes. - -This describes the current Showcase implementation. A completed browser session and a successful phone-verification result are separate concepts; see [Verification results and current limitations](#verification-results-and-current-limitations). - -## Scope and configuration - -The primary walkthrough is the mobile web flow, where the browser follows redirects directly. Desktop IP verification uses a QR code and the same backend callback; see [Desktop QR variant](#desktop-qr-variant). SIM/TS43, SMS OTP, native mobile login, and Android Play Integrity have separate entry points and are not part of this walkthrough. - -A configured client must have `user_flow: "pvn_ip"` and the appropriate scope. The following is an illustrative configuration fragment; replace every placeholder with your own values: - -```json -{ - "realm": "ipification", - "auth_servers": [ - { "id": "stage", "url": "https://auth.example.test/auth" } - ], - "client_id": "", - "client_secret": "", - "clients": [ - { - "user_flow": "pvn_ip", - "title": "IP Phone Number Verification", - "scope": "openid ip:phone_verify" - } - ] -} -``` - -Client-specific credentials override the shared credentials. Otherwise, request locals supply the shared values. Register the exact callback URL for your application origin and selected server, for example: - -```text -https://showcase.example.test/auth/callback/pvn_ip/stage -``` - -Sources: [sample configuration](../config/default.json.sample), [request locals](../middleware/appLocals.js), [configuration reference](CONFIG_DEFAULT_JSON_SAMPLE.md), and [local setup](QUICKSTART.md). - -## End-to-end sequence - -```mermaid -sequenceDiagram - actor User - participant Browser - participant Showcase - participant IPification - participant Cache as In-memory result cache - - User->>Browser: Enter phone number and choose IP - Browser->>Showcase: GET /auth/start?user_flow=pvn_ip&phone=...&state=...&server_id=stage - Showcase-->>Browser: 302 to authorization URL with login_hint and redirect_uri - Browser->>IPification: Follow authorization redirect - IPification-->>Browser: Redirect to callback with code and state - Browser->>Showcase: GET /auth/callback/pvn_ip/stage?code=...&state=... - Showcase->>IPification: POST /token (authorization_code grant) - IPification-->>Showcase: access_token - Showcase->>IPification: POST /userinfo (access_token) - IPification-->>Showcase: Userinfo claims - Showcase->>Cache: Store formatted result, then raw userinfo - Showcase-->>Browser: 302 /auth/complete?state=... - Browser->>Showcase: GET /auth/complete?state=... - Showcase->>Cache: Read formatted result by state - Showcase->>Showcase: Populate browser session - Showcase-->>Browser: 302 /user/info - Browser->>Showcase: GET /user/info - Showcase-->>Browser: Render userinfo page -``` - -The carrier-side verification itself is outside this repository. The code here builds the authorization request and consumes the token/userinfo responses; it does not implement the provider's internal verification process. - -## 1. Enter a phone number and choose IP - -The PNV section renders a phone input and an IP button with `data-user-flow="pvn_ip"`. The browser click handler: - -1. Reads and validates the number using `intl-tel-input` (the existing `999` test dial code bypasses invalid-number rejection). -2. Removes spaces and the leading `+` from the submitted number. -3. Reads the selected auth server. -4. Generates a state value and navigates to `/auth/start` on mobile. - -For example, an entered `+1 202-555-0123` is submitted as `12025550123` after the input library formats it. An illustrative request is: - -```http -GET /auth/start?user_flow=pvn_ip&server_id=stage&phone=12025550123&state=example-state -``` - -`example-state` is a placeholder for explanation, not a recommended production state value. The current frontend generates state with `randomstring(10)`. The UI requires a phone number for this PNV flow; the backend `/auth/start` handler itself does not enforce a phone-number requirement or repeat the frontend number validation. - -Sources: [PNV input and buttons in `login.pug`](../views/login.pug), [button handler and `randomstring` in `login.js`](../public/js/login.js). - -## 2. Build the authorization URL and redirect - -The `start` handler resolves `server_id` and finds the client by `user_flow`. If `server_id` is omitted, it uses the first configured server. The auth service then builds this endpoint: - -```text -{auth_server.url}/realms/{realm}/protocol/openid-connect/auth -``` - -The request includes `response_type=code`, `client_id`, `scope`, `redirect_uri`, the browser's `state`, a generated `nonce`, and the existing locale/consent/environment fields. The phone parameter becomes `login_hint`: - -```js -// Excerpts from services/authService.js -redirect_uri: `${baseUrl}/auth/callback/${userFlow}/${server.id}`, -// ... -if (phone) params.login_hint = phone; -``` - -The backend sends HTTP 302, and the browser follows the authorization URL. For `pvn_ip`, the scope comes from configuration, normally `openid ip:phone_verify`. - -The callback origin is derived from the incoming request's protocol and Host header through `res.locals.baseUrl`. Use the intended public origin when testing redirects and configuring the provider. - -Sources: [`start` in `webHandlers.js`](../routes/auth/webHandlers.js), [`buildAuthorizationUrl` in `authService.js`](../services/authService.js), [`createAppLocalsMiddleware`](../middleware/appLocals.js). - -## 3. Receive the provider callback - -After the provider flow, the browser reaches the callback URL with an authorization code and state: - -```http -GET /auth/callback/pvn_ip/stage?code=example-code&state=example-state -``` - -Both path segments, `userFlow` and `serverId`, are required by the registered route. The callback resolves the selected server and configured client again. If the provider returns `error` or `error_description`, it stores a fixed user-facing error message in the session and redirects to `/auth/login`. - -The code is exchanged by the backend in the following steps; the browser does not perform the token request. - -Sources: [route registration](../routes/auth.js), [`callback` and `CALLBACK_ERROR_MESSAGES` in `webHandlers.js`](../routes/auth/webHandlers.js). - -## 4. Exchange the code for an access token - -The callback calls `exchangeAuthorizationCode` with the selected server, realm, client credentials, code, and the callback URI. The token endpoint is: - -```text -{auth_server.url}/realms/{realm}/protocol/openid-connect/token -``` - -The backend POSTs form-encoded fields: - -```text -code=example-code -redirect_uri=https://showcase.example.test/auth/callback/pvn_ip/stage -grant_type=authorization_code -client_id= -client_secret= -``` - -The fields above are shown one per line for readability; the actual body is URL-encoded. For example, this reproduces the request shape with placeholders: - -```bash -curl 'https://auth.example.test/auth/realms/ipification/protocol/openid-connect/token' \ - --data-urlencode 'code=' \ - --data-urlencode 'redirect_uri=https://showcase.example.test/auth/callback/pvn_ip/stage' \ - --data-urlencode 'grant_type=authorization_code' \ - --data-urlencode 'client_id=' \ - --data-urlencode 'client_secret=' -``` - -These calls are normally made by Showcase. The example is for inspecting the contract, not an additional step after Showcase has exchanged the same code. - -Sources: [`exchangeAuthorizationCode`](../services/authService.js), [`exchangeCodeForToken`](../utils/httpClient.js). - -## 5. Fetch userinfo with the access token - -The HTTP client takes `access_token` from the token response, then POSTs it as a form field to the userinfo endpoint: - -```text -POST {auth_server.url}/realms/{realm}/protocol/openid-connect/userinfo -Content-Type: application/x-www-form-urlencoded - -access_token= -``` - -This implementation passes the token in the form body. An illustrative userinfo response is: - -```json -{ - "sub": "example-subject", - "phone_number": "+12025550123", - "phone_number_verified": "true" -} -``` - -Actual claims and value types depend on the provider response. Showcase stores and displays the returned userinfo without normalizing `phone_number_verified`. - -Source: [`exchangeCodeAndGetUserInfo` and `getUserInfo`](../utils/httpClient.js). - -### Client integration: decide whether phone verification succeeded - -If you are integrating your application with IPification, a successful userinfo request is not enough to mark the phone number as verified. After retrieving userinfo, check `phone_number_verified` before completing your application's authentication flow: - -- **`true`**: Phone verification succeeded; continue the successful authentication flow. -- **`false`**: Phone verification failed. Return a verification error or direct the user to another authentication method, such as SMS OTP, according to your application's flow. -- **Missing or an unrecognized value**: Verification is not confirmed; handle it as an unsuccessful result rather than granting access. - -Perform this decision in your integrating backend before creating an authenticated application session or issuing an application token. Match the claim's representation to your provider contract. This illustrative function explicitly accepts boolean `true` and the string `'true'`, while rejecting boolean `false`, the string `'false'`, and missing values: - -```js -// Example integration logic; this is not currently applied by the Showcase web flow. -function evaluatePnvResult(userInfo) { - const verified = userInfo?.phone_number_verified === true - || userInfo?.phone_number_verified === 'true'; - - if (!verified) { - return { success: false, error: 'PHONE_NUMBER_NOT_VERIFIED' }; - } - - return { success: true, phoneNumber: userInfo.phone_number }; -} -``` - -The caller can turn the unsuccessful result into an error response or start the alternative authentication flow. Avoid a truthiness check such as `if (userInfo.phone_number_verified)`: the string `'false'` is truthy in JavaScript. - -This is the integration rule for deciding PNV success. The following steps describe how Showcase currently caches and displays the response; its ordinary web flow does not apply this claim check before setting the demo session flag. - -## 6. Store the callback result - -The callback creates a display result and saves two entries, in this order: - -| Cache key | Value | Consumer | -| --- | --- | --- | -| `{state}` | `{ userInfo: prettyHtml(userInfo), client_id, client_title, state }` | Browser session completion | -| `{state}-user-info` | Raw userinfo object | `/auth/s2s/signin` | - -Both writes finish before the callback redirects or notifies a QR client. The current store is process-local memory with a default TTL of one hour. Reads do not consume the entries; a process restart loses them. - -An ordinary web callback then redirects to: - -```text -/auth/complete?state=example-state -``` - -Sources: [`callback`](../routes/auth/webHandlers.js), [`saveResult`](../services/authResultStore.js), [cache initialization](../lib/data_store.js). - -## 7. Complete the browser session and display userinfo - -The `complete` handler looks up the formatted result by state. It polls up to five times and waits one second after every miss, including the final miss. When a result is found, it executes: - -```js -req.session.isAuthenticated = true; -req.session.userData = response; -res.redirect('/user/info'); -``` - -If no result is found, it redirects to `/`. The root route redirects to `/auth/login`. - -`GET /user/info` checks `req.session.isAuthenticated`, then renders the stored HTML-formatted userinfo. It is a demo result page, not a separate phone-verification decision. - -Sources: [`waitForResult`](../services/authResultStore.js), [`complete`](../routes/auth/webHandlers.js), [root redirect](../routes/index.js), [user info route](../routes/user.js), [result template](../views/info.pug). - -## Desktop QR variant - -On desktop, selecting IP displays a QR code instead of immediately redirecting that browser. The QR link contains the same `/auth/start` parameters, with state shaped as `-qrcode`. - -The phone scans the link and completes the provider flow. The backend callback still exchanges the code, fetches userinfo, and writes both cache entries. It then emits: - -```js -// Socket.IO room: auth:; event: messages -{ event_name: 'url', url: '/auth/complete?state=' } -``` - -The desktop receives the URL, navigates to `/auth/complete`, and adopts the result into its own browser session. The phone is redirected to `/auth/qrcode/error` only when `userInfo.phone_number_verified === 'false'`; otherwise it goes to `/auth/qrcode/complete`. That comparison is specifically against the string `'false'`, not boolean `false`. - -A callback carrying `ip-backchannel-im-auth: true` takes precedence over the QR branch and returns an empty HTTP 200 after storing the result. That branch supports the backchannel flow rather than the ordinary IP browser walkthrough. - -Sources: [`showQrcodeWithLink` and click handling](../public/js/login.js), [desktop socket listener](../public/js/socket.js), [socket room setup](../socket.js), [`handleQrCodeCallback` and `callback`](../routes/auth/webHandlers.js). - -## Verification results and current limitations - -These points describe the current standard web flow, not the separate signed-state mechanism used by Android Play Integrity: - -- **State correlates results, but is not checked against a saved browser-session value.** The frontend generates it; `/auth/start` checks that it is present and forwards it. The callback does not compare the returned state to an original value saved in the session. If callback state is absent, it generates a UUID. The result cache is then keyed by that state. Do not describe this implementation as completed CSRF state validation. -- **Code exchange and userinfo retrieval do not by themselves constitute the application's phone-verification decision.** The provider returns the claims, but the ordinary web callback does not require `phone_number_verified` to be true, compare the returned phone to the submitted phone, or validate an ID token locally before storing the result. -- **`isAuthenticated` marks the demo browser session.** `/auth/complete` sets it when a cached display result is found. It does not independently check `phone_number_verified`. The result page can therefore display a failed or missing verification claim even though the session flag is true. -- **The QR result-page check is narrower.** It checks the string `'false'` only; it is not a general normalization or validation of the provider's verification claim. - -An integrating application needs an explicit decision about the returned verification claims before granting access or treating a phone number as verified. This guide documents existing behavior and does not change it. - -## Failure paths - -| Stage | Current response or behavior | -| --- | --- | -| Phone missing/invalid in the PNV UI | Show the relevant input modal; do not start the redirect | -| Unknown `server_id` at `/auth/start` | HTTP 400 JSON `{ "error": "INVALID_SERVER_ID" }` | -| No server configured at `/auth/start` | HTTP 400 JSON `{ "error": "NO_AUTH_SERVERS_CONFIGURED" }`; normal app startup also validates configuration | -| Unknown `user_flow` at `/auth/start` or callback | Plain-text `Client not found` with the existing default HTTP 200 | -| State missing at `/auth/start` after server/client resolution | Redirect to `/auth/login` | -| Provider reports callback `error` or `error_description` | Save a fixed session error message and redirect to `/auth/login` | -| Unknown callback server | HTTP 400 text describing the invalid server ID | -| Token exchange/userinfo request fails in the callback | HTTP 400 containing the caught error message | -| Completion cache misses on all attempts | Redirect to `/`, then the login page | - -The fixed provider-error mapping and the token-request failure response are different paths. The callback does not redirect every error to login. - -## Code and test map - -| Step | Implementation | Regression coverage | -| --- | --- | --- | -| Phone input, number formatting, mobile/QR branch | [`login.pug`](../views/login.pug), [`login.js`](../public/js/login.js) | Frontend behavior described from source; not covered by the backend tests below | -| Resolve credentials/server and build authorization URL | [`appLocals.js`](../middleware/appLocals.js), [`authService.js`](../services/authService.js) | [`appLocals.test.js`](../test/middleware/appLocals.test.js), [`authService.test.js`](../test/services/authService.test.js), [`authFlows.test.js`](../test/routes/authFlows.test.js) | -| Callback, cache writes, QR notification, session completion | [`webHandlers.js`](../routes/auth/webHandlers.js), [`authResultStore.js`](../services/authResultStore.js) | [`authFlows.test.js`](../test/routes/authFlows.test.js), [`authResultStore.test.js`](../test/services/authResultStore.test.js) | -| Provider error mapping | [`webHandlers.js`](../routes/auth/webHandlers.js) | [`authSecurity.test.js`](../test/routes/authSecurity.test.js) | - -The backend tests use local provider fixtures. They establish request/response behavior and ordering, not live carrier verification or a deployed end-to-end browser flow. diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md deleted file mode 100644 index 17bb699..0000000 --- a/docs/QUICKSTART.md +++ /dev/null @@ -1,226 +0,0 @@ -# Quick Start Guide - -This guide will help you get the IPification Showcase application up and running quickly. - -## Table of Contents - -- [Prerequisites Checklist](#prerequisites-checklist) -- [5-Minute Setup](#5-minute-setup) - - [Step 1: Install Dependencies](#step-1-install-dependencies) - - [Step 2: Configure Application](#step-2-configure-application) - - [Step 3: Start the Server](#step-3-start-the-server) - - [Step 4: Access the Application](#step-4-access-the-application) -- [Auth Server Selection](#auth-server-selection) -- [Testing Different Flows](#testing-different-flows) - - [Phone Number Verification (PNV)](#phone-number-verification-pnv) - - [Quick Access Login](#quick-access-login) - - [Identity Services](#identity-services) -- [Common Issues](#common-issues) -- [Development Mode](#development-mode) -- [Environment Variables](#environment-variables) -- [Next Steps](#next-steps) -- [Getting Help](#getting-help) - ---- - -## Prerequisites Checklist - -- [ ] Node.js >= 24.0.0 installed (24.22.0 is pinned in `.tool-versions` and Docker) -- [ ] npm installed -- [ ] IPification account credentials - ---- - -## 5-Minute Setup - -### Step 1: Install Dependencies - -```bash -npm install -``` - -### Step 2: Configure Application - -```bash -cp config/default.json.sample config/default.json -``` - -For a full field reference, see [Sample Config Reference](CONFIG_DEFAULT_JSON_SAMPLE.md). - -Edit `config/default.json` and add client configuration - -### Step 3: Start the Server - -```bash -npm start -``` - -### Step 4: Access the Application - -1. Open browser and navigate to: - - ``` - http://localhost:3000 - ``` - -2. **Select Auth Server** (if multiple servers configured): - - A dropdown appears at the top of the page showing available auth servers (e.g., "stage", "live") - - Select the server you want to use for authentication - - Your selection is saved and persists across page reloads - -3. Click "IP" button in PNV tab to test authentication - ---- - -## Auth Server Selection - -If you configured multiple auth servers in `config/default.json`, you can switch between them: - -1. **Dropdown Location**: Top of the login page, next to the logo -2. **Selection Persistence**: Your choice is saved to browser localStorage -3. **Reload Behavior**: The selected server remains active after page reload -4. **Per-Request**: Each authentication request uses your selected server - -**Example Use Cases**: - -- **Testing**: Switch between `stage` and `live` servers without config changes -- **Development**: Test against different environments easily -- **Demo**: Show different server behaviors to clients - -**Single Server**: If only one server is configured, no dropdown appears - the app uses that server automatically. - ---- - -## Testing Different Flows - -### Phone Number Verification (PNV) - -1. Go to **PNV** tab -2. Enter the phone number to verify -3. Click desired button: - - **IP**: Carrier-based verification - - **IP+**: Enhanced verification with mobile ID - - **IM**: Instant messaging verification - - **SIM**: SIM card verification - -For the IP flow's redirects, callback, code exchange, and result/session behavior, see [PNV via IP](PNV.md). - -### Quick Access Login - -1. Go to **Login** tab -2. Click desired login method: - - **IP**: Carrier-based login - - **IP+**: Enhanced login - - **IM**: Instant messaging login - - **SIM**: SIM card login - -### Identity Services - -1. Go to **Identity** tab -2. For **Anonymous Identity**: Click button directly -3. For **KYC Data**: Enter phone number, then click "KYC" - ---- - -## Common Issues - -### Port Already in Use - -**Error**: `Port 3000 is already in use` - -**Solution**: - -```bash -PORT=3001 npm start -``` - -Then access: `http://:3001` - -### Configuration Error - -**Error**: `Configuration error: auth_servers must be defined as an array in config` - -**Solution**: - -- Ensure `auth_servers` array is defined in `config/default.json` -- Must contain at least one server with `id` and `url` fields -- See [Sample Config Reference](CONFIG_DEFAULT_JSON_SAMPLE.md) for proper format - -**Error**: `Client not found` - -**Solution**: - -- Verify `config/default.json` exists -- Check that `user_flow` matches configuration -- Ensure client credentials are correct -- Reference [Sample Config Reference](CONFIG_DEFAULT_JSON_SAMPLE.md) for required fields - -### Cannot Access from Mobile Device - -**Issue**: Mobile device cannot reach the application - -**Solutions**: - -1. Ensure desktop and mobile are on same network -2. Check firewall settings -3. Use machine's local IP (not `localhost` or `127.0.0.1`) -4. Verify port is not blocked - -### Authentication Fails - -**Issue**: Authentication redirects to error page - -**Solutions**: - -1. Verify `redirect_uri` in IPification dashboard matches: - - ``` - http://localhost:3000/auth/callback// - ``` - - Example: `http://localhost:3000/auth/callback/pvn_ip/stage` - -2. Check client_id and client_secret are correct -3. Verify scope permissions match client configuration -4. Ensure phone number format is E.164 (e.g., `+1234567890`) -5. Confirm the selected auth server is accessible and properly configured - ---- - -## Development Mode - -For development with auto-reload: - -```bash -npm run dev -``` - -This uses `nodemon` to automatically restart the server when files change. - -## Environment Variables - -Create a `.env` file for custom configuration: - -```bash -PORT=3000 -NODE_ENV=development -``` - -Supported `NODE_ENV` values: `development`, `stage`, `production`. - ---- - -## Next Steps - -- Read [README.md](../README.md) for detailed documentation -- Check [API.md](API.md) for API endpoint details -- Review [ARCHITECTURE.md](ARCHITECTURE.md) for system design -- Explore different user flows in the application - ---- - -## Getting Help - -- **IPification Developer Portal**: https://developer.ipification.com/ -- **IPification Website**: https://www.ipification.com/ -- Check application logs for detailed error messages diff --git a/docs/SMS.md b/docs/SMS.md deleted file mode 100644 index 113db63..0000000 --- a/docs/SMS.md +++ /dev/null @@ -1,345 +0,0 @@ -# SMS Verification Flow - -Guide for implementing SMS-based phone number verification using CIBA (Client-Initiated Backchannel Authentication) with an OTP code. - -## Table of Contents - -- [What is the SMS flow?](#what-is-the-sms-flow) -- [Quick Setup](#quick-setup) -- [How to Use](#how-to-use) -- [Configuration](#configuration) -- [Frontend Integration](#frontend-integration) -- [Technical Details](#technical-details) -- [API Endpoints](#api-endpoints) -- [Troubleshooting](#troubleshooting) -- [Related Documentation](#related-documentation) - ---- - -## What is the SMS flow? - -The SMS flow uses **CIBA with channel `sms`** so the auth server sends an OTP to the user’s phone. The user enters the code in the app to complete verification. - -- **SMS OTP** – Auth server sends a one-time code via SMS -- **CIBA** – Same OpenID Connect CIBA grant as other flows, with `channel: 'sms'` -- **Callback** – Backend submits the user-entered code to the auth server callback before token exchange -- **Scope** – Typically `openid ip:phone_verify` for phone verification - -**Use for**: Phone number verification (PVN), login with SMS OTP, KYC. - ---- - -## Quick Setup - -### 1. Configure Auth Servers - -In `config/default.json`: - -```json -{ - "auth_servers": [ - { - "id": "stage", - "url": "https://api.stage.ipification.com/auth" - } - ] -} -``` - -### 2. Configure Client - -Add an SMS client: - -```json -{ - "clients": [ - { - "user_flow": "pvn_sms", - "title": "SMS Phone Number Verification", - "scope": "openid ip:phone_verify" - } - ] -} -``` - -### 3. UI - -The app shows an SMS button when `pvn_sms` is in the PNV flows. User enters phone number, starts the flow, receives SMS, then enters OTP in the popup. - ---- - -## How to Use - -### User flow - -1. User selects auth server (if multiple). -2. User enters phone number and clicks the **SMS** button. -3. Backend starts CIBA with `channel: 'sms'`; auth server sends OTP via SMS. -4. User receives SMS with OTP (e.g. “Your verification PIN is: 123456”). -5. App shows “Enter OTP code” popup; user types the code and confirms. -6. Backend sends the code to the auth server callback, then exchanges for tokens and loads user info. -7. User is redirected to `/user/info`. - -### Developer flow - -```text -1. POST /sms/auth → start CIBA (channel sms), get auth_req_id + nonce -2. User gets SMS and enters OTP -3. POST /sms/token → send code to auth callback, then token + userinfo -4. Optional: POST /sms/log → log flow data -``` - ---- - -## Configuration - -### Required config - -- **auth_servers**: At least one server (see [CONFIG_DEFAULT_JSON_SAMPLE.md](CONFIG_DEFAULT_JSON_SAMPLE.md)). -- **client_id** and **client_secret**: OAuth client used for CIBA. -- **realm**: Usually `"ipification"`. -- **clients**: One entry with `user_flow: "pvn_sms"` (and optional `title`, `scope`). - -### Client scope - -SMS verification typically uses: - -- `openid ip:phone_verify` - -The message template (e.g. “Your verification PIN is: {{code}}”) is set in the backend when calling CIBA auth (see [Technical Details](#technical-details)). - ---- - -## Frontend Integration - -### Login button (Pug) - -The SMS button is rendered when `pvn_sms` is in the PNV flows and a client has `user_flow: "pvn_sms"`: - -```pug -button#pvn_sms.btn_sms(data-user-flow='pvn_sms' data-client-id=client.client_id) SMS -``` - -### JavaScript (login.js) - -- **start_pvn_sms(client_id, phone_number)** - Builds payload with `login_hint` (phone), `client_id`, `scope`, `server_id` and calls `start_sms_flow(data)`. - -- **start_sms_flow(data)** - 1. `POST /sms/auth` with `data` → get `auth_req_id`, `nonce`. - 2. Show SweetAlert2 “Enter OTP code” dialog. - 3. On confirm, `POST /sms/token` with `code`, `auth_req_id`, `client_id`, `nonce`, `server_id`. - 4. On success, redirect to `/user/info`. Optionally call `log_sms_data()`. - -- **log_sms_data(data)** - `POST /sms/log` with `{ data }` for logging. - -Phone number is validated (e.g. intl-tel-input) before starting the flow. - ---- - -## Technical Details - -### Architecture overview - -The app only talks to the showcase backend. The backend talks to the IPification auth server. - -1. **Client → Backend**: `POST /sms/auth` (phone as `login_hint`, client_id, scope, server_id). -2. **Backend → Auth server**: CIBA auth with `channel: 'sms'` and message template. -3. **Auth server**: Sends SMS with OTP to the phone. -4. **Backend → Client**: Returns `auth_req_id`, `nonce`, and `auth_server` info. -5. **User**: Enters OTP in the popup. -6. **Client → Backend**: `POST /sms/token` with OTP `code`, `auth_req_id`, `client_id`, `nonce`, `server_id`. -7. **Backend → Auth server**: `POST .../ext/bc/sms/callback` with `{ code }`, `Authorization: Bearer {auth_req_id}`. -8. **Backend → Auth server**: `POST .../token` with CIBA grant → get `access_token`. -9. **Backend → Auth server**: `GET .../userinfo` with `access_token`. -10. **Backend → Client**: User info JSON; backend sets session and returns response. - -### CIBA auth request (backend) - -Backend calls the auth server CIBA auth endpoint with form body including: - -- `client_id`, `client_secret` -- `scope` (e.g. `openid ip:phone_verify`) -- `login_hint`: user phone number -- `channel`: `'sms'` -- `message`: template, e.g. `'Your verification PIN is: {{code}}'` - -Response contains `auth_req_id`, which is used in the callback and token exchange. - -### Callback (backend) - -Before token exchange, the backend must submit the user-entered OTP to the auth server so it can complete the CIBA consent: - -- **URL**: `{authServerUrl}/realms/{realm}/protocol/openid-connect/ext/bc/sms/callback` -- **Method**: POST -- **Headers**: `Authorization: Bearer {auth_req_id}`, `Content-Type: application/json` -- **Body**: `{ "code": "" }` - -After a successful callback, the backend uses the same `auth_req_id` in the token endpoint. - -### Token exchange (backend) - -Same as other CIBA flows: - -- **URL**: `{authServerUrl}/realms/{realm}/protocol/openid-connect/token` -- **Method**: POST -- **Body**: `client_id`, `client_secret`, `grant_type: 'urn:openid:params:grant-type:ciba'`, `auth_req_id` - -Then userinfo is fetched with the returned `access_token`. - ---- - -## API Endpoints - -### 1. Start SMS auth - -**POST** `/sms/auth` - -Starts the CIBA flow with SMS channel. Auth server sends OTP to the given phone. - -#### Request - -- **Headers**: `Content-Type: application/json` -- **Body**: - -```json -{ - "client_id": "your_client_id", - "server_id": "stage", - "login_hint": "+1234567890", - "scope": "openid ip:phone_verify" -} -``` - -| Field | Type | Required | Description | -| ------------ | ------ | -------- | ------------------------------------------------ | -| `client_id` | string | Yes | OAuth2 client ID | -| `server_id` | string | No | Auth server ID; defaults to first server | -| `login_hint` | string | Yes | User phone number (E.164) | -| `scope` | string | No | Scopes; defaults to client’s configured scope | - -#### Response (200 OK) - -```json -{ - "auth_server": { "id": "stage", "url": "https://api.stage.ipification.com/auth" }, - "auth_req_id": "", - "nonce": "" -} -``` - -| Field | Type | Description | -| -------------- | ------ | ------------------------ | -| `auth_server` | object | Selected auth server | -| `auth_req_id` | string | CIBA auth request ID | -| `nonce` | string | Session/correlation id | - -#### Error responses - -- **400**: Invalid or missing `server_id`, or no auth servers configured. -- **401**: Client not found (invalid `client_id`). -- **5xx**: Auth server error; body may include `error`, `status`, `data` (upstream response). - ---- - -### 2. Exchange OTP for token and user info - -**POST** `/sms/token` - -Sends the user-entered OTP to the auth server callback, then performs CIBA token exchange and returns user info. - -#### Request - -- **Headers**: `Content-Type: application/json` -- **Body**: - -```json -{ - "code": "123456", - "auth_req_id": "", - "client_id": "your_client_id", - "nonce": "", - "server_id": "stage" -} -``` - -| Field | Type | Required | Description | -| ------------- | ------ | -------- | ---------------------------------- | -| `code` | string | Yes | OTP from SMS | -| `auth_req_id` | string | Yes | From `/sms/auth` response | -| `client_id` | string | Yes | OAuth2 client ID | -| `nonce` | string | Yes | From `/sms/auth` response | -| `server_id` | string | No | Auth server ID | - -#### Response (200 OK) - -```json -{ - "auth_server": { "id": "stage", "url": "..." }, - "sub": "", - "phone_number": "+1234567890", - "phone_number_verified": true -} -``` - -User info fields depend on scope and auth server. Session is set for `/user/info`. - -#### Error responses - -- **400**: Invalid or missing `server_id`, or no auth servers configured. -- **401**: Client not found. -- **4xx/5xx**: Callback or token/userinfo error; body includes `error`, `status`, and optionally `data`. Session may still be updated with error payload for display. - ---- - -### 3. Log SMS flow data - -**POST** `/sms/log` - -Optional endpoint to log client-side flow data (e.g. token response) for debugging or analytics. - -#### Request - -- **Headers**: `Content-Type: application/json` -- **Body**: `{ "data": { ... } }` - -#### Response - -- **200**: Body `"OK"`. Logging is implementation-specific (e.g. server logs). - ---- - -## Troubleshooting - -### "Invalid server_id" or "No auth servers configured" - -- Ensure `auth_servers` in config has at least one entry and that `server_id` (if sent) matches one of the `id` values. - -### "Client not found" (401) - -- Check that `client_id` matches a configured client and that credentials (e.g. `client_secret`) are correct for that client. - -### No SMS received - -- Confirm auth server is configured to send SMS and that the number is in E.164 format. -- Check auth server logs and SMS provider/configuration (rate limits, number format, country). - -### OTP rejected or "Unable to verify OTP code" - -- User must enter the exact code from the SMS before it expires. -- Ensure the backend calls the SMS callback with the same `auth_req_id` before calling the token endpoint. -- Check auth server callback and token endpoints for detailed error in response `data`. - -### Session / user info not updated - -- On token error, the backend may still set session with error payload; check response body and session handling in `routes/sms.js` and `/user/info`. - ---- - -## Related Documentation - -- [TS43 SIM Verification](TS43.md) – SIM-based verification (no SMS). -- [API Reference](API.md) – Other endpoints. -- [Architecture](ARCHITECTURE.md) – System overview. -- [Configuration](CONFIG_DEFAULT_JSON_SAMPLE.md) – Auth servers and clients. diff --git a/docs/TS43.md b/docs/TS43.md deleted file mode 100644 index 949784e..0000000 --- a/docs/TS43.md +++ /dev/null @@ -1,647 +0,0 @@ -# TS43 SIM Verification Guide - -Quick guide for implementing SIM-based phone number verification (no SMS needed). - -## Table of Contents - -- [What is TS43?](#what-is-ts43) -- [Quick Setup](#quick-setup) -- [How to Use](#how-to-use) -- [Configuration](#configuration) -- [Frontend Integration](#frontend-integration) -- [Troubleshooting](#troubleshooting) -- [Technical Details](#technical-details) - ---- - -## What is TS43? - -TS43 enables **SIM-based phone verification** without SMS codes: - -✅ **No SMS** - Uses SIM card directly -✅ **Seamless** - User just confirms on their device -✅ **Secure** - Device-backed credentials -✅ **Fast** - No waiting for SMS delivery - -**Perfect for**: Login flows, phone verification, KYC processes - ---- - -## Quick Setup - -### 1. Configure Auth Servers - -In `config/default.json`: - -```json -{ - "auth_servers": [ - { - "id": "stage", - "url": "https://api.stage.ipification.com/auth" - } - ] -} -``` - -### 2. Configure Client - -Add SIM flows to your clients: - -```json -{ - "clients": [ - { - "user_flow": "pvn_sim", - "title": "SIM Verification", - "scope": "openid ip:phone_verify" - } - ] -} -``` - -### 3. That's it! - -The app handles TS43 flow automatically. Users will see "SIM" buttons in the UI. - ---- - -## How to Use - -### User Flow (from user perspective): - -1. User clicks **"SIM"** button -2. Browser requests credential from device -3. User confirms on device -4. App shows verified phone number - -**No SMS, no typing!** - -### Developer Flow (from your code): - -```javascript -// Frontend automatically calls: -1. POST /ts43/auth → get digital_request -2. navigator.credentials.get(digital_request) → get vp_token -3. POST /ts43/token → get user info - -// You don't need to implement these - they're already in login.js -``` - ---- - -## Configuration - -### Required Config Fields - -See [CONFIG_DEFAULT_JSON_SAMPLE.md](CONFIG_DEFAULT_JSON_SAMPLE.md) for full config guide. - -**Minimum required**: - -- `auth_servers`: At least one server -- `client_id` and `client_secret`: Your OAuth credentials -- `realm`: Usually `"ipification"` - ---- - -## Frontend Integration - -### HTML Button (already included) - -```pug -button#pvn_sim SIM Verification -``` - -### JavaScript Handler (already included in login.js) - -The app already handles TS43 flow. The relevant code is in: - -- `public/js/login.js` - See `start_pvn_sim()` and `start_login_sim()` functions - -**You don't need to write frontend code** - it's already implemented! - ---- - -## Troubleshooting - -### Error: "Invalid server_id" - -**Cause**: The `server_id` doesn't exist in your `auth_servers` config. - -**Solution**: Check `config/default.json` and ensure server_id matches configured servers. - -### Error: "No auth servers configured" - -**Cause**: `auth_servers` array is empty or missing. - -**Solution**: Add at least one server. See [Quick Setup](#quick-setup). - -### SIM button not showing - -**Cause**: Client not configured or device/browser compatibility. - -**Solution**: - -- Check `clients` array includes `pvn_sim` or `login_sim` -- Test on Android with Chrome/Edge (best compatibility) -- Ensure mobile data is enabled - -### Authentication fails or times out - -**Solution**: - -- Verify OAuth credentials are correct -- Check auth server is accessible -- Confirm carrier supports TS43 -- Try with different SIM card - ---- - -## Technical Details - -### Architecture Overview - -### Key Stages - -IMPORTANT: The client app calls only `/ts43/auth` and `/ts43/token`. All calls to IPification Service are done by the backend. - -1. Client -> Backend: `POST /ts43/auth` -2. Backend -> IPification: `ciba/auth` -3. Backend -> IPification: `ts43/dcql` -4. Backend -> Client: `auth_req_id` + `digital_request` -5. Client -> Credential Manager: request credential -6. Credential Manager -> Client: `vp_token` -7. Client -> Backend: `POST /ts43/token` -8. Backend -> IPification: `ts43/callback` -9. Backend -> IPification: `openid-connect/token` -10. Backend -> IPification: `openid-connect/userinfo` -11. Backend -> Client: user info response - ---- - -## Prerequisites - -### Backend Requirements - -- `client_id` and `client_secret` (configured in `config/default.json`) -- `realm` (default: `ipification`) -- `auth_servers` configuration with at least one server - ---- - -## Integration Steps - -### End-to-end flow (frontend → backend → IPification) - -- Frontend calls `POST /ts43/auth` on your backend with `client_id`, `server_id` (optional), and optional `login_hint`, `carrier_hint`, `scope`, `operation` -- Backend resolves auth server URL from `server_id` (defaults to first server if not provided) -- Backend calls IPification CIBA auth: `POST /ext/ciba/auth` → gets `auth_req_id` -- Backend calls IPification DCQL: `POST /ext/bc/ts43/dcql` with `operation` + `nonce` → gets `dcqlResponse.data` -- Backend returns to frontend: `auth_req_id`, `nonce`, and `digital_request` (built from `dcqlResponse.data`) -- Frontend calls Credential Manager with `digital_request` → receives `vp_token` -- Frontend calls `POST /ts43/token` with `vp_token`, `auth_req_id`, `client_id`, `nonce`, `server_id` (optional) -- Backend calls IPification callback: `POST /ext/bc/ts43/callback` -- Backend exchanges token: `POST /openid-connect/token` → gets `access_token` -- Backend fetches user info: `GET /openid-connect/userinfo` -- Backend returns user info JSON to frontend - ---- - -### Step 1: Call `/ts43/auth` - -**Goal:** Start the CIBA flow and receive a `digital_request` for the Credential Manager. - -**Client request body** - -```json -{ - "client_id": "your_client_id", - "server_id": "stage", - "login_hint": "+1234567890", - "carrier_hint": "51010", - "scope": "openid ip:phone_verify", - "operation": "VerifyPhoneNumber" -} -``` - -**Backend response body** - -```json -{ - "auth_req_id": "", - "nonce": "", - "digital_request": { - "protocol": "openid4vp-v1-unsigned", - "data": { - "response_type": "vp_token", - "response_mode": "dc_api", - "nonce": "", - "dcql_query": { - "credentials": [] - } - } - } -} -``` - -**How the response is used** - -- `auth_req_id`: used later in `/ts43/token` to exchange for an access token -- `nonce`: pass back to `/ts43/token` for session correlation -- `digital_request`: sent to the Credential Manager to obtain `vp_token` - -**How `digital_request` is built** - -After the backend calls DCQL (`/ext/bc/ts43/dcql`), it embeds `dcqlResponse.data` into the DC API request: - -```json -{ - "protocol": "openid4vp-v1-unsigned", - "data": { - "response_type": "vp_token", - "response_mode": "dc_api", - "nonce": "", - "dcql_query": { - "credentials": [""] - } - } -} -``` - -### Step 2: Get VP Token via Credential Manager - -**Goal:** Use `digital_request` to retrieve `vp_token` in the browser. - -**Client request (browser)** - -```js -const { auth_req_id, digital_request, nonce } = body; - -const credentialResponse = await navigator.credentials.get({ - digital: { - requests: [digital_request], - }, -}); -``` - -**Extract `vp_token` from the response** - -```js -const credentialData = credentialResponse.token || credentialResponse.data; -const { vp_token } = credentialData || {}; - -const dataToken = { - vp_token: vp_token["ipification.com"][0], - auth_req_id, - client_id: data.client_id, - nonce, -}; -``` - -### Step 3: Call `/ts43/token` - -**Goal:** Send `vp_token` + `auth_req_id` to complete CIBA and fetch user info. - -**Client request body** - -```json -{ - "vp_token": "", - "auth_req_id": "", - "client_id": "your_client_id", - "nonce": "" -} -``` - -**Important**: The `nonce` value must be the same `nonce` returned from `/ts43/auth` in Step 1. This is used for session correlation and security validation. - -**Backend response body** - -```json -{ - "sub": "", - "phone_number": "+1234567890", - "phone_number_verified": true -} -``` - -**How the response is used** - -- The client uses the user info JSON for login/session creation -- The backend also stores the response in the session for `/user/info` - ---- - -## API Endpoints - -### 1) CIBA Authentication Endpoint - -**POST** `/ts43/auth` - -This is the first client call. Backend initiates CIBA and DCQL with IPification Service. - -#### Request Headers - -``` -Content-Type: application/json -``` - -#### Request Body - -```json -{ - "client_id": "your_client_id", - "server_id": "stage", - "login_hint": "+1234567890", - "carrier_hint": "carrierX", - "scope": "openid ip:phone_verify", - "operation": "VerifyPhoneNumber" -} -``` - -**Request Body Fields**: - -| Field | Type | Required | Description | -| -------------- | ------ | -------- | -------------------------------------------------------------------- | -| `client_id` | string | Yes | Your OAuth2 client ID | -| `server_id` | string | No | Auth server ID (e.g., `"stage"`, `"live"`). Defaults to first server | -| `login_hint` | string | No | Phone number in E.164 format (e.g., `+1234567890`) | -| `carrier_hint` | number | No | Carrier MCC+MNC code (e.g., `51010` for Viettel Vietnam) | -| `operation` | string | No | Operation type: `"VerifyPhoneNumber"` or `"GetPhoneNumber"` | -| `scope` | string | No | OAuth scopes (default: uses client's scope from configuration) | - -#### Parameters - -| Parameter | Type | Required | Description | -| -------------- | ------ | -------- | --------------------------------------- | -| `client_id` | string | Yes | OAuth client id | -| `login_hint` | string | No | E.164 phone for VerifyPhoneNumber | -| `carrier_hint` | string | No | Carrier hint (MCCMNC or vendor string) | -| `scope` | string | No | Example: `openid ip:phone_verify` | -| `operation` | string | No | `VerifyPhoneNumber` or `GetPhoneNumber` | - -#### Response (Success - 200 OK) - -```json -{ - "auth_req_id": "", - "nonce": "", - "digital_request": { - "protocol": "openid4vp-v1-unsigned", - "data": { - "response_type": "vp_token", - "response_mode": "dc_api", - "nonce": "", - "dcql_query": { - "credentials": [] - } - } - } -} -``` - -#### Response Fields - -| Field | Type | Description | -| ----------------- | ------ | ------------------------- | -| `auth_req_id` | string | Auth request id from CIBA | -| `nonce` | string | UUID for the DCQL request | -| `digital_request` | object | OpenID4VP DC API request | - -#### Backend -> IPification Calls - -##### A) CIBA Auth - -- URL: `${AUTH_SERVER_URL}/realms/ipification/protocol/openid-connect/ext/ciba/auth` -- Method: `POST` -- Content-Type: `application/x-www-form-urlencoded` - -Form body: - -- `client_id` -- `client_secret` -- `scope` -- `login_hint` (optional) -- `carrier_hint` (optional) - -Example: - -```bash -curl -X POST "$AUTH_SERVER_URL/realms/ipification/protocol/openid-connect/ext/ciba/auth" \ - -H "Content-Type: application/x-www-form-urlencoded" \ - --data-urlencode "client_id=$CLIENT_ID" \ - --data-urlencode "client_secret=$CLIENT_SECRET" \ - --data-urlencode "scope=openid" \ - --data-urlencode "login_hint=+1234567890" \ - --data-urlencode "carrier_hint=carrierX" -``` - -##### B) TS43 DCQL - -- URL: `${AUTH_SERVER_URL}/realms/ipification/protocol/openid-connect/ext/bc/ts43/dcql` -- Method: `POST` -- Content-Type: `application/json` -- Authorization: `Bearer ${auth_req_id}` - -JSON body: - -```json -{ - "operation": "GetPhoneNumber", - "nonce": "" -} -``` - -Example: - -```bash -curl -X POST "$AUTH_SERVER_URL/realms/ipification/protocol/openid-connect/ext/bc/ts43/dcql" \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $AUTH_REQ_ID" \ - -d '{"operation":"GetPhoneNumber","nonce":""}' -``` - ---- - -### 2) Token Exchange Endpoint - -**POST** `/ts43/token` - -Client sends the VP token and auth request id. Backend validates and returns user info. - -#### Request Headers - -``` -Content-Type: application/json -``` - -#### Request Body - -```json -{ - "vp_token": "", - "auth_req_id": "", - "client_id": "your_client_id", - "nonce": "", - "server_id": "stage" -} -``` - -**Request Body Fields**: - -| Field | Type | Required | Description | -| ------------- | ------ | -------- | -------------------------------------------------------- | -| `vp_token` | string | Yes | VP token received from Credential Manager | -| `auth_req_id` | string | Yes | Auth request ID from `/ts43/auth` response | -| `client_id` | string | Yes | Your OAuth2 client ID | -| `nonce` | string | Yes | Nonce from `/ts43/auth` response | -| `server_id` | string | No | Auth server ID. Defaults to first server if not provided | - -#### Parameters - -| Parameter | Type | Required | Description | -| ------------- | ------ | -------- | --------------------------------- | -| `vp_token` | string | Yes | VP token from Credential Manager | -| `auth_req_id` | string | Yes | Auth request id from `/ts43/auth` | -| `client_id` | string | Yes | OAuth client id | -| `nonce` | string | Yes | Nonce returned from `/ts43/auth` | - -#### Response (Success - 200 OK) - -```json -{ - "sub": "", - "phone_number": "+1234567890", - "phone_number_verified": true -} -``` - -#### Response Fields - -| Field | Type | Description | -| ----------------------- | ------- | --------------------- | -| `sub` | string | Subject identifier | -| `phone_number` | string | Verified phone number | -| `phone_number_verified` | boolean | Verification result | - -#### Error Response - -```json -{ - "error": "", - "status": 500, - "data": {} -} -``` - -`data` is included when the upstream service returns a body. - -#### Backend -> IPification Calls - -##### A) TS43 Callback - -- URL: `${AUTH_SERVER_URL}/realms/ipification/protocol/openid-connect/ext/bc/ts43/callback` -- Method: `POST` -- Content-Type: `application/json` -- Authorization: `Bearer ${auth_req_id}` - -JSON body: - -```json -{ - "vp_token": "" -} -``` - -Expected response (typical): - -```json -{ - "status": "ok" -} -``` - -Some deployments may return an empty body or vendor-specific payload. A 2xx response is sufficient. - -##### B) CIBA Token Exchange - -- URL: `${AUTH_SERVER_URL}/realms/ipification/protocol/openid-connect/token` -- Method: `POST` -- Content-Type: `application/x-www-form-urlencoded` - -Form body: - -- `client_id` -- `client_secret` -- `grant_type` = `urn:openid:params:grant-type:ciba` -- `auth_req_id` - -Success response (typical): - -```json -{ - "access_token": "", - "expires_in": 300, - "refresh_expires_in": 0, - "token_type": "Bearer", - "scope": "openid" -} -``` - -Error response (typical): - -```json -{ - "error": "authorization_pending", - "error_description": "The authorization request is still pending." -} -``` - -##### C) User Info - -- URL: `${AUTH_SERVER_URL}/realms/ipification/protocol/openid-connect/userinfo` -- Method: `GET` -- Authorization: `Bearer ${access_token}` - -Success response (typical): - -```json -{ - "sub": "", - "phone_number": "+1234567890", - "phone_number_verified": true -} -``` - ---- - ---- - -## Troubleshooting - -### Error: "Invalid server_id: 'xyz'" - -**Cause**: The `server_id` provided doesn't exist in `auth_servers` configuration. - -**Solution**: Check `config/default.json` and ensure the `server_id` matches one of the configured server IDs (e.g., `stage`, `live`). - -### Error: "server_id is required" - -**Cause**: The `server_id` field is missing from the request body (only applies if backend has strict validation). - -**Solution**: Include `server_id` in your request body or configure the backend to use the default server. - -### Error: "No auth servers configured" - -**Cause**: The `auth_servers` array in configuration is empty or undefined. - -**Solution**: Add at least one auth server to `config/default.json`. See [Prerequisites](#prerequisites) for proper configuration. - ---- - -## Related Documentation - -- [API Documentation](API.md): Complete API endpoint reference -- [Architecture Documentation](ARCHITECTURE.md): System design and architecture -- [Sample Config Reference](CONFIG_DEFAULT_JSON_SAMPLE.md): Configuration guide -- [Quick Start Guide](QUICKSTART.md): Getting started guide 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/e2e/auth-error.spec.ts b/e2e/auth-error.spec.ts new file mode 100644 index 0000000..2d3a636 --- /dev/null +++ b/e2e/auth-error.spec.ts @@ -0,0 +1,65 @@ +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=${attempt.state}&code=browser-invalid-client`, + ); + await expect(page).toHaveURL(/\/auth\/error\?/); + await expect( + page.getByRole("heading", { + name: "The application could not sign in", + exact: true, + }), + ).toBeVisible(); + await expect(page.getByText(/Check the client credentials/)).toBeVisible(); + await page.getByText("Error details", { exact: true }).click(); + await expect( + page.getByText("CLIENT_AUTH_FAILED", { exact: true }), + ).toBeVisible(); + await expect(page.getByText("401", { exact: true })).toBeVisible(); + await expect(page.getByText("invalid_client", { exact: true })).toBeVisible(); + await expect(page.getByText(/Support reference:/)).toBeVisible(); + await expect(page.locator("body")).not.toContainText( + "PRIVATE_PROVIDER_DIAGNOSTIC", + ); + await page.screenshot({ + path: "test-results/auth-error-desktop.png", + fullPage: true, + }); + await page.setViewportSize({ width: 375, height: 812 }); + await expect( + page.getByRole("link", { name: "Try again", exact: true }), + ).toBeVisible(); + expect( + await page.evaluate(() => document.documentElement.scrollWidth), + ).toBeLessThanOrEqual(375); + await page.screenshot({ + path: "test-results/auth-error-mobile.png", + fullPage: true, + }); + 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/e2e/bootstrap.spec.ts b/e2e/bootstrap.spec.ts new file mode 100644 index 0000000..bee129e --- /dev/null +++ b/e2e/bootstrap.spec.ts @@ -0,0 +1,7 @@ +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/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/e2e/sms.spec.ts b/e2e/sms.spec.ts new file mode 100644 index 0000000..eb4caab --- /dev/null +++ b/e2e/sms.spec.ts @@ -0,0 +1,63 @@ +import { test, expect } from "@playwright/test"; +test("SMS dialog supports digit filtering, cancel and successful manual OTP", async ({ + page, +}) => { + await page.goto("/auth/login#pnv"); + await page.getByLabel("Phone number", { exact: true }).fill("2025550123"); + await page.getByRole("button", { name: "SMS", exact: true }).click(); + const dialog = page.getByRole("dialog"); + await expect(dialog).toBeVisible(); + await dialog.getByLabel("OTP code", { exact: true }).fill("ab123456"); + await expect(dialog.getByLabel("OTP code", { exact: true })).toHaveValue( + "123456", + ); + await dialog.getByRole("button", { name: "Verify", exact: true }).click(); + await expect(page).toHaveURL(/\/user\/info$/); +}); +test("OTP retry stays in dialog and WebOTP is aborted when cancelled", async ({ + page, +}) => { + await page.addInitScript(() => { + (window as any).OTPCredential = class {}; + (window as any).otpAborted = false; + Object.defineProperty(navigator.credentials, "get", { + configurable: true, + value: (options: any) => + new Promise((_, reject) => + options.signal.addEventListener("abort", () => { + (window as any).otpAborted = true; + reject(new DOMException("Aborted", "AbortError")); + }), + ), + }); + }); + await page.goto("/auth/login#pnv"); + await page.getByLabel("Phone number", { exact: true }).fill("2025550123"); + await page.getByRole("button", { name: "SMS", exact: true }).click(); + const dialog = page.getByRole("dialog"); + await expect(dialog.getByLabel("OTP code", { exact: true })).toBeEnabled(); + await dialog.getByRole("button", { name: "Cancel", exact: true }).click(); + await expect(dialog).not.toBeVisible(); + expect(await page.evaluate(() => (window as any).otpAborted)).toBe(true); + await expect(page).toHaveURL(/auth\/login/); +}); +test("wrong OTP remains retryable without leaving the dialog", async ({ + page, +}) => { + await page.route("**/sms/token", (route) => + route.fulfill({ + status: 401, + contentType: "application/json", + body: JSON.stringify({ error: "Incorrect OTP" }), + }), + ); + await page.goto("/auth/login#pnv"); + await page.getByLabel("Phone number", { exact: true }).fill("2025550123"); + await page.getByRole("button", { name: "SMS", exact: true }).click(); + await page.getByLabel("OTP code", { exact: true }).fill("000000"); + await page.getByRole("button", { name: "Verify", exact: true }).click(); + await expect( + page.getByRole("alert").filter({ hasText: /Incorrect OTP|cancelled/ }), + ).toContainText("Incorrect OTP"); + await expect(page.getByRole("dialog")).toBeVisible(); +}); diff --git a/e2e/stage.spec.ts b/e2e/stage.spec.ts new file mode 100644 index 0000000..f1c1a01 --- /dev/null +++ b/e2e/stage.spec.ts @@ -0,0 +1,38 @@ +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("stage keeps Wonderland 999 country and disabled selector option", async ({ + page, +}) => { + const redis = await startRedis(), + config = configFixture({ disabled_select_country: true }); + 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"); + 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 }), + ).toBeEnabled(); + await expect(page.locator(".iti")).toContainText("+999"); + } finally { + await app.stop(); + config.remove(); + await redis.stop(); + } +}); 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/e2e/ts43.spec.ts b/e2e/ts43.spec.ts new file mode 100644 index 0000000..58ebb05 --- /dev/null +++ b/e2e/ts43.spec.ts @@ -0,0 +1,44 @@ +import { test, expect } from "@playwright/test"; +test("TS43 sends credential response to token endpoint and renders results", async ({ + page, +}) => { + await page.addInitScript(() => { + Object.defineProperty(navigator.credentials, "get", { + value: async () => ({ + data: { vp_token: { "ipification.com": ["fixture-vp"] } }, + }), + configurable: true, + }); + }); + 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 ({ + page, +}) => { + await page.addInitScript(() => { + Object.defineProperty(navigator.credentials, "get", { + value: async () => null, + configurable: true, + }); + }); + await page.goto("/auth/login#login"); + await page.getByRole("button", { name: "SIM", exact: true }).click(); + await expect( + page.getByRole("alert").filter({ hasText: /Incorrect OTP|cancelled/ }), + ).toContainText("cancelled"); + await expect( + page.getByRole("button", { name: "SIM", exact: true }), + ).toBeEnabled(); +}); 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/e2e/visual.spec.ts b/e2e/visual.spec.ts new file mode 100644 index 0000000..e7a8d10 --- /dev/null +++ b/e2e/visual.spec.ts @@ -0,0 +1,15 @@ +import { test } from "@playwright/test"; +test("capture desktop and mobile migration previews", async ({ page }) => { + await page.setViewportSize({ width: 1440, height: 1000 }); + await page.goto("/auth/login"); + await page.getByLabel("Phone number", { exact: true }).waitFor(); + await page.screenshot({ + path: "test-results/showcase-desktop.png", + fullPage: true, + }); + await page.setViewportSize({ width: 375, height: 812 }); + await page.screenshot({ + path: "test-results/showcase-mobile.png", + fullPage: true, + }); +}); diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..4fc822f --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,16 @@ +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/**", + ".superpowers/**", + ".local-backups/**", + "next-env.d.ts", + "test-results/**", + "playwright-report/**", + ]), + { rules: { "@typescript-eslint/no-explicit-any": "off" } }, +]); diff --git a/lib/data_store.js b/lib/data_store.js deleted file mode 100644 index cff6302..0000000 --- a/lib/data_store.js +++ /dev/null @@ -1,8 +0,0 @@ -const { createCache } = require('cache-manager'); - -// Create memory-only cache -const dataStore = createCache({ - ttl: 60 * 60 * 1000, // 1 hour TTL -}); - -module.exports = dataStore; diff --git a/middleware/appLocals.js b/middleware/appLocals.js deleted file mode 100644 index d080957..0000000 --- a/middleware/appLocals.js +++ /dev/null @@ -1,32 +0,0 @@ -const { getAuthServer } = require('../utils/authServerHelper'); - -/** Build the request locals used by page rendering and authentication handlers. */ -function createAppLocalsMiddleware({ config, locale }) { - return function appLocals(req, res, next) { - const { client_id, client_secret, clients } = config; - const customClients = clients.map(client => ({ - ...client, - client_id: client.client_id || client_id, - client_secret: client.client_secret || client_secret, - })); - - res.locals = { - ...res.locals, - title: 'IPification Showcase', - ...config, - clients: customClients, - locale, - baseUrl: `${req.protocol}://${req.headers.host}`, - get_flow_title: (user_flow, default_title) => { - const client = config.clients.find(item => item.user_flow === user_flow); - return client ? client.title : default_title; - }, - getAuthServer: (serverId) => getAuthServer(config.auth_servers, serverId), - app_env: process.env.NODE_ENV || 'development', - }; - - next(); - }; -} - -module.exports = { createAppLocalsMiddleware }; diff --git a/middleware/ts43RateLimit.js b/middleware/ts43RateLimit.js deleted file mode 100644 index b53817e..0000000 --- a/middleware/ts43RateLimit.js +++ /dev/null @@ -1,65 +0,0 @@ -const crypto = require('crypto'); -const { rateLimit } = require('express-rate-limit'); - -const DEFAULT_WINDOW_MS = 60_000; -const DEFAULT_IP_MAX_REQUESTS = 20; -const DEFAULT_IDENTITY_MAX_REQUESTS = 5; - -function readPositiveInteger(value, fallback) { - const parsed = Number.parseInt(value, 10); - return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback; -} - -function hashLoginHint(loginHint) { - return crypto.createHash('sha256').update(String(loginHint || '')).digest('hex'); -} - -function createRateLimitHandler() { - return (req, res, _next, options) => { - const resetTime = req.rateLimit?.resetTime; - const retryAfter = resetTime - ? Math.max(1, Math.ceil((resetTime.getTime() - Date.now()) / 1000)) - : Math.ceil(options.windowMs / 1000); - - res.set('Retry-After', String(retryAfter)); - res.status(options.statusCode).json({ error: 'RATE_LIMITED' }); - }; -} - -function createTs43AuthRateLimiter(options = {}) { - const windowMs = readPositiveInteger(options.windowMs || process.env.TS43_RATE_LIMIT_WINDOW_MS, DEFAULT_WINDOW_MS); - const maxRequests = readPositiveInteger(options.maxRequests, 0); - const ipMaxRequests = maxRequests || readPositiveInteger(options.ipMaxRequests || process.env.TS43_RATE_LIMIT_IP_MAX, DEFAULT_IP_MAX_REQUESTS); - const identityMaxRequests = maxRequests || readPositiveInteger(options.identityMaxRequests || process.env.TS43_RATE_LIMIT_IDENTITY_MAX, DEFAULT_IDENTITY_MAX_REQUESTS); - const handler = createRateLimitHandler(); - - const ipLimiter = rateLimit({ - windowMs, - limit: ipMaxRequests, - identifier: 'ts43-auth-ip', - standardHeaders: 'draft-7', - legacyHeaders: false, - handler, - }); - const identityLimiter = rateLimit({ - windowMs, - limit: identityMaxRequests, - identifier: 'ts43-auth-identity', - standardHeaders: 'draft-7', - legacyHeaders: false, - keyGenerator: (req) => `${req.body?.client_id || ''}:${hashLoginHint(req.body?.login_hint)}`, - handler, - }); - - return (req, res, next) => { - ipLimiter(req, res, (error) => { - if (error) { - next(error); - return; - } - identityLimiter(req, res, next); - }); - }; -} - -module.exports = { createTs43AuthRateLimiter }; diff --git a/next-env.d.ts b/next-env.d.ts new file mode 100644 index 0000000..a419cbe --- /dev/null +++ b/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/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/package-lock.json b/package-lock.json index 492183b..dfc6d45 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,2132 +1,7195 @@ { - "name": "ipification-demo", - "version": "1.2.16-SNAPSHOT", + "name": "ipification-showcase-next", + "version": "1.2.17-SNAPSHOT", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "ipification-demo", - "version": "1.2.16-SNAPSHOT", - "dependencies": { - "axios": "^1.20.0", - "cache-manager": "^7.2.9", + "name": "ipification-showcase-next", + "version": "1.2.17-SNAPSHOT", + "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", + "axios": "^1.21.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cn": "^0.5.0", "config": "^5.0.1", - "cookie-parser": "^1.4.7", - "debug": "^4.4.3", - "dotenv": "^18.0.8", - "express": "^5.3.0", - "express-rate-limit": "^8.7.1", - "express-session": "^1.19.0", - "firebase-admin": "^14.5.0", "geoip-lite": "^2.0.3", "google-auth-library": "^11.3.1", - "helmet": "^8.3.0", - "html-entities": "^2.6.0", - "http-errors": "^2.0.1", - "json-pretty-html": "^1.1.6", + "intl-tel-input": "^29.5.7", "jsonwebtoken": "^9.0.3", - "keyv": "^6.1.0", - "morgan": "^1.12.1", - "nocache": "^4.0.0", - "pug": "^3.0.4", - "socket.io": "^4.8.4", - "stylus": "^0.64.0", - "uuid": "^14.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", + "tailwind-merge": "^3.7.0" }, "devDependencies": { - "nodemon": "^3.1.14" - }, + "@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", + "prettier": "^3.9.10", + "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": ">=24.0.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@adobe/css-tools": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.3.tgz", - "integrity": "sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==", - "license": "MIT" - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "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/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "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/types": "^7.28.5" - }, - "bin": { - "parser": "bin/babel-parser.js" + "@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.0.0" + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "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/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@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/@cacheable/utils": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", - "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", - "license": "MIT", - "dependencies": { - "hashery": "^1.5.1", - "keyv": "^5.6.0" - } - }, - "node_modules/@cacheable/utils/node_modules/keyv": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", - "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "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": { - "@keyv/serialize": "^1.1.1" + "@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/@fastify/busboy": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.2.2.tgz", - "integrity": "sha512-yXSS27qPExaXeuLvMRMXOLtpipzfQYNjG3FkunDWKGfMYjKuhFXko9CVzqxm8jcF+lmtS9Fd89QNdh9XDjnbNg==", - "license": "MIT" - }, - "node_modules/@firebase/app-check-interop-types": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.5.tgz", - "integrity": "sha512-qId34pVZ2CXTmtu4ofW5leiI93DvnOvIcr/5GT7MZPw5WXAi6nZoU+g9cNRgl7K90UJSbK0cXxten4meYMPyZg==", - "license": "Apache-2.0" - }, - "node_modules/@firebase/app-types": { - "version": "0.9.6", - "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.6.tgz", - "integrity": "sha512-yPLahy7Esfu2w/yme3msVK4xTkDXQqq6szfQn8yVOQpCKiT5GVFjqNpLbuz6NkX0WuTwUidkmPewW3r4xkvpeg==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/logger": "0.5.2" + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" } }, - "node_modules/@firebase/auth-interop-types": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.6.tgz", - "integrity": "sha512-FgwZqDrBqgK0BHI70QTv1v/5wmuDF9f8fsJFvKSxoRlK2PPfSQtZAk2jhXu4pe9BZzFi/e4UYusU/bEQHdf6Qg==", - "license": "Apache-2.0" - }, - "node_modules/@firebase/component": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.5.tgz", - "integrity": "sha512-vuFDcL91Q+2ZuBJkyOh86T4q0B4ffNTDjc/A38tybO56odQABxRTFLTIowCWqAKeIcgo37GowWMVgFF73gD8Qw==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/util": "1.15.3", - "tslib": "^2.1.0" - }, + "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": ">=20.0.0" + "node": ">=6.9.0" } }, - "node_modules/@firebase/database": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.5.tgz", - "integrity": "sha512-/JGpvszLoNXNgzilRXocigGfFF4hbcPA9wN1i1kjJx6oKkXgkHteZYl3lQs1lJX3ETDf6bv7zI15lPMVUp4sAQ==", - "license": "Apache-2.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": { - "@firebase/app-check-interop-types": "0.3.5", - "@firebase/auth-interop-types": "0.2.6", - "@firebase/component": "0.7.5", - "@firebase/logger": "0.5.2", - "@firebase/util": "1.15.3", - "faye-websocket": "0.11.4", - "tslib": "^2.1.0" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { - "node": ">=20.0.0" + "node": ">=6.9.0" } }, - "node_modules/@firebase/database-compat": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.7.tgz", - "integrity": "sha512-lBq9sJm8MnJINKJnkAKSOj2MbC66xGoSVCcGkPtstl+lkoKEBtD46qHLbuDgW/WbLPoKVm17RIN8BxHj+Z2F2w==", - "license": "Apache-2.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": { - "@firebase/component": "0.7.5", - "@firebase/database": "1.1.5", - "@firebase/database-types": "1.0.22", - "@firebase/logger": "0.5.2", - "@firebase/util": "1.15.3", - "tslib": "^2.1.0" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { - "node": ">=20.0.0" + "node": ">=6.9.0" }, "peerDependencies": { - "@firebase/app": "0.x", - "@firebase/app-compat": "0.x" - }, - "peerDependenciesMeta": { - "@firebase/app": { - "optional": true - }, - "@firebase/app-compat": { - "optional": true - } + "@babel/core": "^7.0.0" } }, - "node_modules/@firebase/database-types": { - "version": "1.0.22", - "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.22.tgz", - "integrity": "sha512-YAZNXsjY9EQQ+pKw/3ax8n5FgolHC7Qew7EY5RceYdl0R2ZP+kCv1O0DkKlcceue8uQCOreHCNN7PWVFpY1Nug==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/app-types": "0.9.6", - "@firebase/util": "1.15.3" + "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/@firebase/logger": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.5.2.tgz", - "integrity": "sha512-J2VO4NFTc0xQFrxV1B/lm5balicm9cwuX2acR9Yn41fN8KgUeQFo+VJV222IqW2FPSXKDu9uo5WdQFWf9TPbYg==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.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": ">=20.0.0" + "node": ">=6.9.0" } }, - "node_modules/@firebase/util": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.15.3.tgz", - "integrity": "sha512-c/z/gaIlaaLZEuGbE6sLUuJ61tskg1JghvhcNQzW948ASBinbVBBRnZTC4b4yt4LaEtJQkYlyzqLHcutFwEIvA==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.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": ">=20.0.0" + "node": ">=6.9.0" } }, - "node_modules/@google-cloud/firestore": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-9.1.0.tgz", - "integrity": "sha512-MYh566cRZY/6/UyRIvpYynRwBBK+UFykw4EojSMt/9P74jr7FSWmupplvxPtiNA3NutyEhcEngZqdAoHgENgxA==", - "license": "Apache-2.0", - "optional": true, + "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": { - "@google-cloud/firestore-api": "^0.2.0", - "@opentelemetry/api": "^1.9.0", - "fast-deep-equal": "^3.1.3", - "functional-red-black-tree": "^1.0.1", - "google-gax": "^6.0.1", - "protobufjs": "^7.5.3" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8" }, "engines": { - "node": ">=22" + "node": ">=6.9.0" } }, - "node_modules/@google-cloud/firestore-api": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@google-cloud/firestore-api/-/firestore-api-0.2.0.tgz", - "integrity": "sha512-wfHJPZmLqzpy6FN6U+7HkkP/AKhGsbHSpEy/qL2TyI3/pKRAvpamQxZ2qvps5hMRgAjfuHCim+oeJj9RveCaqg==", - "license": "Apache-2.0", - "optional": true, + "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": { - "google-gax": "^5.0.0" + "@babel/types": "^7.29.11" + }, + "bin": { + "parser": "bin/babel-parser.js" }, "engines": { - "node": ">=18" + "node": ">=6.0.0" } }, - "node_modules/@google-cloud/firestore-api/node_modules/gaxios": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.3.1.tgz", - "integrity": "sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ==", - "license": "Apache-2.0", - "optional": true, + "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": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "node-fetch": "^3.3.2" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@google-cloud/firestore-api/node_modules/google-auth-library": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz", - "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==", - "license": "Apache-2.0", - "optional": true, + "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": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^7.0.0", - "gcp-metadata": "^8.0.0", - "google-logging-utils": "^1.0.0", - "gtoken": "^8.0.0", - "jws": "^4.0.0" + "@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": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@google-cloud/firestore-api/node_modules/google-gax": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-5.0.8.tgz", - "integrity": "sha512-M4vpZcXQIC1gqIVGQ7eaU3jXQA6zecStyTXu514TYfThlgSurYJOxHZo9fzU6hAgwPWvuEynAVHWyaIk80VeEA==", - "license": "Apache-2.0", - "optional": true, + "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": { - "@grpc/grpc-js": "^1.12.6", - "@grpc/proto-loader": "^0.8.0", - "duplexify": "^4.1.3", - "google-auth-library": "10.5.0", - "google-logging-utils": "1.1.3", - "node-fetch": "^3.3.2", - "object-hash": "^3.0.0", - "proto3-json-serializer": "3.0.4", - "protobufjs": "^7.5.4", - "retry-request": "^8.0.2", - "rimraf": "^5.0.1" + "@babel/helper-string-parser": "^7.29.11", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@google-cloud/firestore-api/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==", + "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": { - "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" + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" } }, - "node_modules/@google-cloud/firestore-api/node_modules/proto3-json-serializer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-3.0.4.tgz", - "integrity": "sha512-E1sbAYg3aEbXrq0n1ojJkRHQJGE1kaE/O6GLA94y8rnJBfgvOPTOd1b9hOceQK1FFZI9qMh1vBERCyO2ifubcw==", - "license": "Apache-2.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": { - "protobufjs": "^7.4.0" - }, - "engines": { - "node": ">=18" + "tslib": "^2.4.0" } }, - "node_modules/@google-cloud/firestore-api/node_modules/retry-request": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-8.0.4.tgz", - "integrity": "sha512-pI6/7eabUYkZxamkOq0g0uMxKLLGnjzhefY+vL8bVXag5rto4OU2YBTPytWLuHH7aEKD6fn7kJycQfid0Mwnkw==", + "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": { - "extend": "^3.0.2", - "teeny-request": "^10.0.0" - }, - "engines": { - "node": ">=18" + "tslib": "^2.4.0" } }, - "node_modules/@google-cloud/firestore-api/node_modules/teeny-request": { - "version": "10.1.4", - "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-10.1.4.tgz", - "integrity": "sha512-R1Cg4Vu0UULeDfHL/kjABLaTW++9yD/B6n2g48y5dJ04hsEaxcfmAqbNDzNsbqAYJyIpZafjklLG9YxRu9uzOg==", - "license": "Apache-2.0", - "optional": true, + "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": { - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.1", - "node-fetch": "^3.3.2", - "stream-events": "^1.0.5" + "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": ">=18" + "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/@google-cloud/paginator": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-7.0.1.tgz", - "integrity": "sha512-k32cWlHAF8yTgg8rciLI8mPMI6UzuJdKp53YRxISRwMFxUl2FYplvs+Mr2UHxKn0W7rXsqZnUZy73AOJFDP8iA==", + "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", - "optional": true, - "dependencies": { - "extend": "^3.0.2" - }, "engines": { - "node": ">=22" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@google-cloud/projectify": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-6.0.1.tgz", - "integrity": "sha512-zA3o4l87UJ56odT/e9PcT4n2bqVnrp6dg9vI75DoGGbl0/YJemX8Fta7454htECt07eALxxc4iaAbrX3T+QulA==", - "license": "Apache-2.0", - "optional": true, + "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": ">=22" + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@google-cloud/promisify": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-6.0.1.tgz", - "integrity": "sha512-lpN2AtoQ/iimp7jjm5zJFWbpW7OJc5qWmQdt59CI4ENeoIRIx+yf2ShSR2kanQfjFOshA74eSbimXXuRaSCUVg==", + "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", - "optional": true, + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, "engines": { - "node": ">=22" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@google-cloud/storage": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-8.1.0.tgz", - "integrity": "sha512-iFDlOGBzmzTelslzcf0yTpoqloyfTUi448PxJXvjtbvGqtc2ftT8HUSl5+sKQPRz70y64ha5FqojDhABP103EA==", + "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", - "optional": true, "dependencies": { - "@google-cloud/paginator": "^7.0.1", - "@google-cloud/projectify": "^6.0.1", - "@google-cloud/promisify": "^6.0.1", - "abort-controller": "^3.0.0", - "async-retry": "^1.3.3", - "duplexify": "^4.1.3", - "fast-xml-parser": "^5.3.4", - "gaxios": "^6.0.2", - "google-auth-library": "^9.6.3", - "html-entities": "^2.5.2", - "mime": "^3.0.0", - "p-limit": "^3.0.1", - "retry-request": "^9.0.1", - "teeny-request": "^11.0.1" + "@eslint/core": "^0.17.0" }, "engines": { - "node": ">=22" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@google-cloud/storage/node_modules/gcp-metadata": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", - "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "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", - "optional": true, "dependencies": { - "gaxios": "^6.1.1", - "google-logging-utils": "^0.0.2", - "json-bigint": "^1.0.0" + "@types/json-schema": "^7.0.15" }, "engines": { - "node": ">=14" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@google-cloud/storage/node_modules/google-auth-library": { - "version": "9.15.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", - "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", - "license": "Apache-2.0", - "optional": true, + "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": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^6.1.1", - "gcp-metadata": "^6.1.0", - "gtoken": "^7.0.0", - "jws": "^4.0.0" + "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": ">=14" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@google-cloud/storage/node_modules/google-logging-utils": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", - "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", - "license": "Apache-2.0", - "optional": true, + "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": ">=14" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@google-cloud/storage/node_modules/gtoken": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", - "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "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", - "optional": true, - "dependencies": { - "gaxios": "^6.0.0", - "jws": "^4.0.0" - }, "engines": { - "node": ">=14.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, - "node_modules/@grpc/grpc-js": { - "version": "1.14.6", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.6.tgz", - "integrity": "sha512-SgC/B3/VZ8lTyOZ4Cwk6f1skWhmkNAaVfiX90qd5u1rRXGKk1//8FTUEN9GvOUq7rfWIqAYkA3CoyumYVpOS6g==", + "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", - "optional": true, - "dependencies": { - "@grpc/proto-loader": "^0.8.0", - "@js-sdsl/ordered-map": "^4.4.2" - }, "engines": { - "node": ">=12.10.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@grpc/proto-loader": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.1.tgz", - "integrity": "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==", + "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", - "optional": true, "dependencies": { - "lodash.camelcase": "^4.3.0", - "long": "^5.0.0", - "protobufjs": "^7.5.5", - "yargs": "^17.7.2" - }, - "bin": { - "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" }, "engines": { - "node": ">=6" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "license": "ISC", + "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": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" + "@floating-ui/utils": "^0.2.12" } }, - "node_modules/@js-sdsl/ordered-map": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", - "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "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", - "optional": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" } }, - "node_modules/@keyv/serialize": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", - "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "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/@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/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==", + "dev": true, + "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/@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/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/@nodable/entities": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-3.0.0.tgz", - "integrity": "sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw==", + "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/nodable" + "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", - "optional": true + "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/@opentelemetry/api": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", - "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "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", - "optional": true, + "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": ">=8.0.0" + "node": ">=6" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "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", - "optional": true, + "bin": { + "cn": "bin/cn.mjs" + }, "engines": { - "node": ">=14" + "node": ">=22" } }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "license": "BSD-3-Clause", - "optional": true + "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/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "license": "BSD-3-Clause", - "optional": true + "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/@protobufjs/codegen": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", - "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", - "license": "BSD-3-Clause", - "optional": true + "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/@protobufjs/eventemitter": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", - "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", - "license": "BSD-3-Clause", - "optional": true + "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/@protobufjs/fetch": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", - "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", - "license": "BSD-3-Clause", - "optional": true, + "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/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": { - "@protobufjs/aspromise": "^1.1.1" + "safe-buffer": "^5.0.1" } }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", - "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", - "license": "BSD-3-Clause", - "optional": true + "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/@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==", + "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/@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==", + "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": { - "@types/node": "*" + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" } }, - "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==", + "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": { - "@types/ms": "*", - "@types/node": "*" + "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/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "25.0.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.2.tgz", - "integrity": "sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA==", + "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": { - "undici-types": "~7.16.0" + "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/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "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", - "dependencies": { - "@types/node": "*" + "engines": { + "node": ">= 0.4" } }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "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", - "optional": true, - "dependencies": { - "event-target-shim": "^5.0.0" - }, "engines": { - "node": ">=6.5" + "node": ">= 0.4" } }, - "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==", + "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": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "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.6" + "node": ">= 0.4" } }, - "node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "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", - "bin": { - "acorn": "bin/acorn" + "dependencies": { + "es-errors": "^1.3.0" }, "engines": { - "node": ">=0.4.0" + "node": ">= 0.4" } }, - "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==", + "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": ">= 14" + "node": ">= 0.4" } }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "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", - "engines": { - "node": ">=12" + "dependencies": { + "hasown": "^2.0.2" }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "engines": { + "node": ">= 0.4" } }, - "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==", + "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": { - "color-convert": "^2.0.1" + "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": ">=8" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "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": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, + "license": "MIT", "engines": { - "node": ">= 8" + "node": ">=6" } }, - "node_modules/anynum": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz", - "integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], + "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", - "optional": true - }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", - "license": "MIT" - }, - "node_modules/assert-never": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.4.0.tgz", - "integrity": "sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==", - "license": "MIT" + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/async-retry": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", - "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "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", - "optional": true, "dependencies": { - "retry": "0.13.1" + "@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/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/axios": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz", - "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", + "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": { - "follow-redirects": "^1.16.0", - "form-data": "^4.0.6", - "https-proxy-agent": "^5.0.1", - "proxy-from-env": "^2.1.0" + "@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/axios/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==", + "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": "4" - }, - "engines": { - "node": ">= 6.0.0" + "debug": "^3.2.7", + "is-core-module": "^2.16.1", + "resolve": "^2.0.0-next.6" } }, - "node_modules/axios/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==", + "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": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" + "ms": "^2.1.1" } }, - "node_modules/babel-walk": { - "version": "3.0.0-canary-5", - "resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz", - "integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==", - "license": "MIT", + "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": { - "@babel/types": "^7.9.6" + "@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": ">= 10.0.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" - }, - "node_modules/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" + "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 }, - { - "type": "consulting", - "url": "https://feross.org/support" + "eslint-plugin-import-x": { + "optional": true } - ], - "license": "MIT" + } }, - "node_modules/basic-auth": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", - "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "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": { - "safe-buffer": "5.1.2" + "ms": "^2.1.3" }, "engines": { - "node": ">= 0.8" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/basic-auth/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "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==", + "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": "*" + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } } }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "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", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "dependencies": { + "ms": "^2.1.1" } }, - "node_modules/body-parser": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", - "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "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": { - "bytes": "^3.1.2", - "content-type": "^2.0.0", - "debug": "^4.4.3", - "http-errors": "^2.0.1", - "iconv-lite": "^0.7.2", - "on-finished": "^2.4.1", - "qs": "^6.15.2", - "raw-body": "^3.0.2", - "type-is": "^2.1.0" + "@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": ">=18" + "node": ">=4" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, - "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==", + "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": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "20 || >=22" + "ms": "^2.1.1" } }, - "node_modules/brace-expansion/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==", + "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": "MIT", - "engines": { - "node": "18 || 20 || >=22" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" } }, - "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==", + "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": { - "fill-range": "^7.1.1" + "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": ">=8" + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, - "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/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "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": ">= 0.8" + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, - "node_modules/cache-manager": { - "version": "7.2.9", - "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-7.2.9.tgz", - "integrity": "sha512-d4vceEyYe95gPxEyQchlEOH9vJlkNRW8G6gzFzzMTxJK9PahYMhC9chrEqgZN0HulROjgw3IzmWVNk7Q7ytiGw==", + "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": { - "@cacheable/utils": "^2.5.0", - "keyv": "^5.6.0" + "@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/cache-manager/node_modules/keyv": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", - "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", - "license": "MIT", - "dependencies": { - "@keyv/serialize": "^1.1.1" + "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/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", + "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": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" }, "engines": { - "node": ">= 0.4" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "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", + "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": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" }, "engines": { - "node": ">= 0.4" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://opencollective.com/eslint" } }, - "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", + "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": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "estraverse": "^5.1.0" }, "engines": { - "node": ">=10" + "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" }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "engines": { + "node": ">=4.0" } }, - "node_modules/character-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", - "integrity": "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==", + "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": { - "is-regex": "^1.0.3" + "@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/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "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": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "@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.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "node": ">=8.6.0" } }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "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", - "optional": true, "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" + "is-glob": "^4.0.1" }, "engines": { - "node": ">=12" + "node": ">= 6" } }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "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", - "optional": true, + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, "engines": { - "node": ">=8" + "node": "^12.20 || >= 14.13" } }, - "node_modules/cliui/node_modules/emoji-regex": { + "node_modules/file-entry-cache": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT", - "optional": true - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "flat-cache": "^4.0.0" }, "engines": { - "node": ">=8" + "node": ">=16.0.0" } }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "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", - "optional": true, "dependencies": { - "ansi-regex": "^5.0.1" + "to-regex-range": "^5.0.1" }, "engines": { "node": ">=8" } }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "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", - "optional": true, "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" }, "engines": { "node": ">=10" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", + "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": { - "color-name": "~1.1.4" + "flatted": "^3.2.9", + "keyv": "^4.5.4" }, "engines": { - "node": ">=7.0.0" + "node": ">=16" } }, - "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/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/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "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": { - "delayed-stream": "~1.0.0" + "is-callable": "^1.2.7" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "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": { - "json5": "^2.2.3" + "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": ">= 20.11.0" + "node": ">= 6" } }, - "node_modules/constantinople": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz", - "integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==", + "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": { - "@babel/parser": "^7.6.0", - "@babel/types": "^7.6.1" + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" } }, - "node_modules/content-disposition": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-2.0.1.tgz", - "integrity": "sha512-e+H0ZXHSWYrENhQzw1LPuP4oF5MzVKmDU6d3hxlvaPEYLLg62MxtQNPRx4SYSuYJSBUgnQIG4HIN2tEtNv7Dog==", + "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": ">=18" - }, + "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": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/content-type": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", - "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "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": ">=18" + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "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", - "engines": { - "node": ">= 0.6" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/cookie-parser": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", - "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", - "license": "MIT", + "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": { - "cookie": "0.7.2", - "cookie-signature": "1.0.6" + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" }, "engines": { - "node": ">= 0.8.0" + "node": ">=22" } }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "license": "MIT" - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "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", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, "engines": { - "node": ">= 0.10" + "node": ">= 14" } }, - "node_modules/cross-spawn": { + "node_modules/gaxios/node_modules/https-proxy-agent": { "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "license": "MIT", "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "agent-base": "^7.1.2", + "debug": "4" }, "engines": { - "node": ">= 8" - } - }, - "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": ">= 14" } }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", + "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": { - "ms": "^2.1.3" + "gaxios": "^8.3.0", + "google-logging-utils": "^2.0.0", + "json-bigint": "^1.0.0" }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=22" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "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.0" + "node": ">= 0.4" } }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "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": ">= 0.8" + "node": ">=6.9.0" } }, - "node_modules/doctypes": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", - "integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==", - "license": "MIT" - }, - "node_modules/dotenv": { - "version": "18.0.8", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-18.0.8.tgz", - "integrity": "sha512-2KDeoFoK/zRtcnbunCDlLG5VWVMcyqeQc6iK65o231nfNAcuGrKoqUQfJFUtBI+QlShk5HKIHxwebBtUl3rEfA==", - "license": "BSD-2-Clause", - "bin": { - "dotenv": "dist/index.cjs" + "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": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" + "node": ">=24.0.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==", + "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.1", + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "gopd": "^1.2.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/duplexify": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", - "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", - "license": "MIT", - "optional": true, - "dependencies": { - "end-of-stream": "^1.4.1", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1", - "stream-shift": "^1.0.2" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "license": "MIT" - }, - "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/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "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": ">= 0.8" + "node": ">=6" } }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "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", - "optional": true, "dependencies": { - "once": "^1.4.0" + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" } }, - "node_modules/engine.io": { - "version": "6.6.10", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.10.tgz", - "integrity": "sha512-9/lX2bdlizlCXMHRMOIm03VBQHQYC7VvydcxtTAUJRxNW1QzM/2PMFSmr6h/lCiMHcyCP6abK+t9Q+j4vekk8Q==", + "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": { - "@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" + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" }, "engines": { - "node": ">=10.2.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "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", - "engines": { - "node": ">=10.0.0" + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", + "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": ">= 0.4" + "node": ">=10.13.0" } }, - "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==", + "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": ">= 0.4" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "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": { - "es-errors": "^1.3.0" + "define-properties": "^1.2.1", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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", + "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": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" + "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": ">= 0.4" + "node": ">=22" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "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", - "optional": true, "engines": { - "node": ">=6" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" + "node_modules/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/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "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.6" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "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", - "optional": true, "engines": { - "node": ">=6" + "node": ">=8" } }, - "node_modules/express": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.3.0.tgz", - "integrity": "sha512-qCqy1xSoaugxOD4IVHb1ZWdkvCF2W3aE2okKouWj8VwJ+ztskSiXoBnCSJqfKT+cVPJhuJqe9YLbY425+Zh00g==", + "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": { - "accepts": "^2.0.0", - "body-parser": "^2.3.0", - "content-disposition": "^2.0.1", - "content-type": "^2.0.0", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.8", - "qs": "^6.16.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.1.0", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" + "es-define-property": "^1.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/express-rate-limit": { - "version": "8.7.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.1.tgz", - "integrity": "sha512-2FXbqkygil3mZiXzEtM0IjE/UBmBNot0J1GEVrJT4JbrvwOSoEtqtO+wHbiMQELWe1eNvHoYRg2dkjOid+6JkA==", + "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": { - "debug": "^4.4.3", - "ip-address": "^10.2.0" + "dunder-proto": "^1.0.0" }, "engines": { - "node": ">= 16" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/express-rate-limit" + "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" }, - "peerDependencies": { - "express": ">= 4.11" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/express-session": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.19.0.tgz", - "integrity": "sha512-0csaMkGq+vaiZTmSMMGkfdCOabYv192VbytFypcvI0MANrp+4i/7yEkJ0sbAEhycQjntaKGzYfjfXQyVb7BHMA==", + "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": { - "cookie": "~0.7.2", - "cookie-signature": "~1.0.7", - "debug": "~2.6.9", - "depd": "~2.0.0", - "on-headers": "~1.1.0", - "parseurl": "~1.3.3", - "safe-buffer": "~5.2.1", - "uid-safe": "~2.1.5" + "has-symbols": "^1.0.3" }, "engines": { - "node": ">= 0.8.0" + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/express-session/node_modules/cookie-signature": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", - "license": "MIT" - }, - "node_modules/express-session/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "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": { - "ms": "2.0.0" + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" } }, - "node_modules/express-session/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "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/express/node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "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": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" + "agent-base": "6", + "debug": "4" }, "engines": { - "node": ">= 0.6" + "node": ">= 6" } }, - "node_modules/express/node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "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": ">=6.6.0" + "node": ">=0.10.0" } }, - "node_modules/express/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "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": ">= 0.6" + "node": ">= 4" } }, - "node_modules/express/node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "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": { - "mime-db": "^1.54.0" + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" }, "engines": { - "node": ">=18" + "node": ">=6" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/express/node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "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.6" - } - }, - "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==", - "license": "MIT" - }, - "node_modules/fast-xml-builder": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.3.1.tgz", - "integrity": "sha512-pIM/1n3ntFXKYrUZwW7QCK0gAW7XY+wzj1YMIV3tLDvPj/V+zTGJK5e3/4WJfwj0qWw2ElNXiTixda/R+3YSug==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "path-expression-matcher": "^1.6.2", - "xml-naming": "^0.3.0" + "node": ">=0.8.19" } }, - "node_modules/fast-xml-parser": { - "version": "5.11.1", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.11.1.tgz", - "integrity": "sha512-TBw6K/fxoQGGjCmZDw9w/ZwP3uDcnTM4YH/g+PFRWr8sbe5idXtxNN6vITh4+1ruCZaho6uBFurElsA7F0zzgw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], + "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", - "optional": true, - "dependencies": { - "@nodable/entities": "^3.0.0", - "fast-xml-builder": "^1.2.0", - "is-unsafe": "^2.0.0", - "path-expression-matcher": "^1.6.2", - "strnum": "^2.4.2", - "xml-naming": "^0.3.0" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, - "node_modules/faye-websocket": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", - "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", - "license": "Apache-2.0", "dependencies": { - "websocket-driver": ">=0.5.1" + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" }, "engines": { - "node": ">=0.8.0" - } - }, - "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" - } - ], + "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": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" }, "engines": { - "node": "^12.20 || >= 14.13" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "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": { - "to-regex-range": "^5.0.1" + "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": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "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": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" + "has-bigints": "^1.0.2" }, "engines": { - "node": ">= 18.0.0" + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/firebase-admin": { - "version": "14.5.0", - "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-14.5.0.tgz", - "integrity": "sha512-DElmgmaH9aPXu2dWXGQiDoan33UfTnbfAAKaue4fwEZxmpdaNrTk5DmuFn+vVDki2KZ6TiaC0IHRWcEzcDxB3Q==", - "license": "Apache-2.0", + "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": { - "@fastify/busboy": "^3.0.0", - "@firebase/database-compat": "^2.1.4", - "@firebase/database-types": "^1.0.20", - "fast-deep-equal": "^3.1.1", - "google-auth-library": "^11.1.0", - "jsonwebtoken": "^9.0.0", - "jwks-rsa": "^4.0.1" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { - "node": ">=22" + "node": ">= 0.4" }, - "optionalDependencies": { - "@google-cloud/firestore": "^9.1.0", - "@google-cloud/storage": "^8.1.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/follow-redirects": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.1.tgz", - "integrity": "sha512-FNvFGzoMLWmE6Yj9spb/zjd7yiNCHiAW9/Tg9CXrQ8wuu32HtlJOwWO11OJafl5FfY3DxTdQ0vj42zU1kvv5jg==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], + "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": ">=4.0" + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "license": "ISC", + "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": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" + "hasown": "^2.0.4" }, "engines": { - "node": ">=14" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "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==", + "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": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.4", - "mime-types": "^2.1.35" + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" }, "engines": { - "node": ">= 6" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "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": { - "fetch-blob": "^3.1.2" + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" }, "engines": { - "node": ">=12.20.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "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.6" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "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.8" + "node": ">=0.10.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==", + "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, - "hasInstallScript": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "call-bound": "^1.0.3" + }, "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/function-bind": { + "node_modules/is-generator-function": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "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/functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "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", - "optional": true - }, - "node_modules/gaxios": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.3.0.tgz", - "integrity": "sha512-p+ggrQw3fBwH2F5N/PAI4k/G/y1art5OxKpb2J2chwNNHM4hHuAOtivjPuirMF4KNKwTTUal/lPfL2+7h2mEcg==", - "license": "Apache-2.0", - "optional": true, "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.9" + "is-extglob": "^2.1.1" }, "engines": { - "node": ">=14" + "node": ">=0.10.0" } }, - "node_modules/gcp-metadata": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", - "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "gaxios": "^7.0.0", - "google-logging-utils": "^1.0.0", - "json-bigint": "^1.0.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": ">=18" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gcp-metadata/node_modules/gaxios": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.3.1.tgz", - "integrity": "sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ==", - "license": "Apache-2.0", - "optional": true, + "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": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "node-fetch": "^3.3.2" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { - "node": ">=18" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gcp-metadata/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==", + "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", - "optional": true, "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/geoip-lite": { + "node_modules/is-set": { "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" - }, + "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": ">=24.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/geoip-lite/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==", + "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": { - "safer-buffer": ">= 2.1.2 < 3.0.0" + "call-bound": "^1.0.3" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", - "optional": true, + "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": "6.* || 8.* || >= 10.*" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "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-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", + "call-bound": "^1.0.2", "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -2135,1597 +7198,1974 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "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": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" + "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/isaacs" + "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "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": "ISC", + "license": "MIT", "dependencies": { - "is-glob": "^4.0.1" + "call-bound": "^1.0.3" }, "engines": { - "node": ">= 6" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", - "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "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": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "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", + "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": { - "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" + "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": ">=22" + "node": ">= 0.4" } }, - "node_modules/google-auth-library/node_modules/gaxios": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-8.3.1.tgz", - "integrity": "sha512-h4/R7ay7JFXU5kmAkp/Y4Y4Vmm7m83tD579GUgfQQsRRGQ4TzSedyHla6XgTWohQuVffT3ldN4FF6uCqlRGaEA==", - "license": "Apache-2.0", + "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": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "node-fetch": "^3.3.2" + "argparse": "^2.0.1" }, - "engines": { - "node": ">=22" + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/google-auth-library/node_modules/gcp-metadata": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-9.0.3.tgz", - "integrity": "sha512-2YYnIlHaKBGT2IPg3G2M57hia9Galz15zsEOvw9T3oRf0lSn6KN6VcHQLqby7x8ksYKnjXvp3rp2KJyLCN6zfQ==", - "license": "Apache-2.0", - "dependencies": { - "gaxios": "^7.1.3", - "google-logging-utils": "^2.0.0", - "json-bigint": "^1.0.0" + "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": ">=22" + "node": ">=6" } }, - "node_modules/google-auth-library/node_modules/gcp-metadata/node_modules/gaxios": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.3.1.tgz", - "integrity": "sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ==", - "license": "Apache-2.0", + "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": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "node-fetch": "^3.3.2" - }, - "engines": { - "node": ">=18" + "bignumber.js": "^9.0.0" } }, - "node_modules/google-auth-library/node_modules/google-logging-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-2.0.1.tgz", - "integrity": "sha512-HMhaQghlOTvbcb3c4T5jmmOMtG3JUF1iOQMezaJXL86CDS+Tm2vHd0IeLFRAx3+ewd+bo9E1HFHoy17X5aJa9A==", - "license": "Apache-2.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": ">=22" + "node": ">=6" } }, - "node_modules/google-auth-library/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==", + "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": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" + "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.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" + "node": ">=12", + "npm": ">=6" } }, - "node_modules/google-gax": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-6.3.0.tgz", - "integrity": "sha512-9/kD2yhNJJDAoxZMw+v/d53W6bibgHXr0uU2RRfkC2/r5uH1sBo1nzuaSUq1lKgipZ3O4HvksA7/rRBvkFHFkA==", - "license": "Apache-2.0", - "optional": true, + "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": { - "@grpc/grpc-js": "^1.12.6", - "@grpc/proto-loader": "^0.8.0", - "@opentelemetry/api": "^1.9.0", - "duplexify": "^4.1.3", - "google-auth-library": "^11.0.0", - "google-logging-utils": "^2.0.0", - "node-fetch": "^3.3.2", - "object-hash": "^3.0.0", - "proto3-json-serializer": "^4.0.0", - "protobufjs": "^7.5.4", - "retry-request": "^9.0.0" + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" }, "engines": { - "node": ">=22" + "node": ">=4.0" } }, - "node_modules/google-gax/node_modules/google-logging-utils": { + "node_modules/jwa": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-2.0.1.tgz", - "integrity": "sha512-HMhaQghlOTvbcb3c4T5jmmOMtG3JUF1iOQMezaJXL86CDS+Tm2vHd0IeLFRAx3+ewd+bo9E1HFHoy17X5aJa9A==", - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=22" + "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/google-gax/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==", + "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", - "optional": true, "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" + "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/google-logging-utils": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", - "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", - "license": "Apache-2.0", - "optional": true, + "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": ">=14" + "node": ">=0.10" } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "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.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.2.0" } }, - "node_modules/gtoken": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", - "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", + "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", - "optional": true, "dependencies": { - "gaxios": "^7.0.0", - "jws": "^4.0.0" + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" }, "engines": { - "node": ">=18" + "node": ">= 0.8.0" } }, - "node_modules/gtoken/node_modules/gaxios": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.3.1.tgz", - "integrity": "sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ==", - "license": "Apache-2.0", - "optional": true, + "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": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "node-fetch": "^3.3.2" + "detect-libc": "^2.0.3" }, "engines": { - "node": ">=18" + "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/gtoken/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", + "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, - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, + "os": [ + "darwin" + ], "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">= 12.0.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/node-fetch" + "url": "https://opencollective.com/parcel" } }, - "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", + "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": ">=8" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "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", + "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": ">= 0.4" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "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" - }, + "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": ">= 0.4" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/hashery": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", - "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", - "license": "MIT", - "dependencies": { - "hookified": "^1.15.0" - }, + "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": ">=20" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "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" - }, + "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": ">= 0.4" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/helmet": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.3.0.tgz", - "integrity": "sha512-Qgpiaws3Sm30Av8Eah6sjMCZZwjlBu+E68rhpCWBshY1lb09HtLwj5GviX0OyQIn+ulUS0iX0AxN5n3tLZzz1w==", - "license": "MIT", + "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": ">=18.0.0" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/EvanHahn" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/hookified": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", - "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", - "license": "MIT" - }, - "node_modules/html-entities": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", - "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/mdevils" - }, - { - "type": "patreon", - "url": "https://patreon.com/mdevils" - } + "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" ], - "license": "MIT" - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, "engines": { - "node": ">= 0.8" + "node": ">= 12.0.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://opencollective.com/parcel" } }, - "node_modules/http-parser-js": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", - "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", - "license": "MIT" - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "license": "MIT", + "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, - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, + "os": [ + "win32" + ], "engines": { - "node": ">= 14" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "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" - }, + "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": ">= 14" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "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": { - "safer-buffer": ">= 2.1.2 < 3.0.0" + "p-locate": "^5.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=10" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", - "dev": true, - "license": "ISC" + "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/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" + "node_modules/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/ip-address": { - "version": "10.7.4", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.4.tgz", - "integrity": "sha512-DWBMKyeM25D70nrIiTkgQth59/G+naKOTep6huF9j+GmeK3DMD60Zvxg5d5qQnz+38AHgou6ean2LECX+lrKDg==", - "license": "MIT", - "engines": { - "node": ">= 12" - } + "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/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } + "node_modules/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/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "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": { - "binary-extensions": "^2.0.0" + "js-tokens": "^3.0.0 || ^4.0.0" }, - "engines": { - "node": ">=8" + "bin": { + "loose-envify": "cli.js" } }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "license": "MIT", + "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": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" + "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" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/is-expression": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-4.0.0.tgz", - "integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==", + "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": { - "acorn": "^7.1.1", - "object-assign": "^4.1.1" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "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, + "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.10.0" + "node": ">= 0.4" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "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": ">= 8" } }, - "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==", + "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": { - "is-extglob": "^2.1.1" + "braces": "^3.0.3", + "picomatch": "^2.3.1" }, "engines": { - "node": ">=0.10.0" + "node": ">=8.6" } }, - "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, + "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.12.0" + "node": ">= 0.6" } }, - "node_modules/is-promise": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", - "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", - "license": "MIT" - }, - "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==", + "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": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" + "mime-db": "1.52.0" }, "engines": { - "node": ">= 0.4" + "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" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": "*" } }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "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", - "optional": true, - "engines": { - "node": ">=8" - }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-unsafe": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.2.tgz", - "integrity": "sha512-HgbIHPBH0KHHCcjLfGsCvhtPTVxjaAZlXjwdz7/GQC40SjSe4sfQsar8J5VFo8JOSbarkpV0OLG95bbaNd9aAQ==", + "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/NaturalIntelligence" + "url": "https://github.com/sponsors/ai" } ], "license": "MIT", - "optional": true - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "bin": { + "nanoid": "bin/nanoid.cjs" }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/jose": { - "version": "6.2.12", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz", - "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==", + "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://github.com/sponsors/panva" + "url": "https://opencollective.com/napi-postinstall" } }, - "node_modules/js-stringify": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", - "integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==", + "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/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==", + "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": { - "bignumber.js": "^9.0.0" - } - }, - "node_modules/json-pretty-html": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/json-pretty-html/-/json-pretty-html-1.1.6.tgz", - "integrity": "sha512-+bhQgNJ3ebvOIGSZ9OPWTHpv9xKaKIodekiphrUKrOCovuBfimtmn2G3vv/jzNsXg12g9SHcBYoiPO362ghDNA==", - "license": "MIT", - "engines": { - "node": ">= 8.0.0" - } - }, - "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", + "@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": { - "json5": "lib/cli.js" + "next": "dist/bin/next" }, "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" + "node": ">=20.9.0" }, - "engines": { - "node": ">=12", - "npm": ">=6" + "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/jstransformer": { + "node_modules/node-domexception": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", - "integrity": "sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==", - "license": "MIT", - "dependencies": { - "is-promise": "^2.0.0", - "promise": "^7.0.1" - } - }, - "node_modules/jwa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "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", - "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" + "engines": { + "node": ">=10.5.0" } }, - "node_modules/jwks-rsa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-4.1.0.tgz", - "integrity": "sha512-sbkByqyATKYJP5F4RXj03N5TUNC0QLTjCAZvwTzC4BwJZ8e0/cWxN8YROnyUth2g1/ONWi4eSFHeu6oYalrc3Q==", + "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": { - "@types/jsonwebtoken": "^9.0.4", - "debug": "^4.3.4", - "jose": "^6.1.3", - "limiter": "^1.1.5", - "lru-cache": "^11.0.0", - "lru-memoizer": "^3.0.0" + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >= 23.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/jwks-rsa/node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" + "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/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==", + "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": { - "jwa": "^2.0.1", - "safe-buffer": "^5.0.1" + "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/keyv": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-6.1.0.tgz", - "integrity": "sha512-0OO64DxKOITdyQljNCcO+vc+iyaPmm3OV5eKOWRxWCp3V0QnXenpqcKfjGjycNILUJNV+erm1ose6Hsgr1fNFA==", + "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", - "dependencies": { - "hookified": "^3.0.3" - }, "engines": { - "node": ">= 22.19.0" + "node": ">=18" } }, - "node_modules/keyv/node_modules/hookified": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/hookified/-/hookified-3.0.4.tgz", - "integrity": "sha512-Uos6DSO6BKv4DeR1C22UosRpeUaaoQgEQ+eDCyoJRll2OGzGpZc708vL7orIvqrYLo6zhVV+pkwO9zKVCsq3PQ==", + "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": ">=22.18.0" + "node": ">=0.10.0" } }, - "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==", + "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.2.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/limiter": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", - "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" - }, - "node_modules/lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "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", - "optional": true - }, - "node_modules/lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", - "license": "MIT" - }, - "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.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/long": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "license": "Apache-2.0", - "optional": true - }, - "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" + "engines": { + "node": ">= 0.4" + } }, - "node_modules/lru-memoizer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-3.0.0.tgz", - "integrity": "sha512-m83w/cYXLdUIboKSPxzPAGfYnk+vqeDYXuoSrQRw1q+yVEd8IXhvMufN8Q5TIPe7e2jyX4SRNrDJI2Skw1yznQ==", + "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": { - "lodash.clonedeep": "^4.5.0", - "lru-cache": "^11.0.1" + "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/lru-memoizer/node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", - "license": "BlueOak-1.0.0", + "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": "20 || >=22" + "node": ">= 0.4" } }, - "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==", + "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/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "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.8" + "node": ">= 0.4" } }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "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": ">=18" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/mime": { + "node_modules/obug": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", - "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "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", - "optional": true, - "bin": { - "mime": "cli.js" - }, "engines": { - "node": ">=10.0.0" + "node": ">=12.20.0" } }, - "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==", + "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.6" + "node": ">= 0.8.0" } }, - "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==", + "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": { - "mime-db": "1.52.0" + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" }, "engines": { - "node": ">= 0.6" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/minimatch": { - "version": "10.2.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", - "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "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": "BlueOak-1.0.0", + "license": "MIT", "dependencies": { - "brace-expansion": "^5.0.8" + "yocto-queue": "^0.1.0" }, "engines": { - "node": "18 || 20 || >=22" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/morgan": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.12.1.tgz", - "integrity": "sha512-tljKC0ex20AjO58Ob/eZ53JloycbVswbVNCHx6V6VLGzqt/w8dIynVGL0G8qVjNKwiA7sYSogCrN6QtJ82IV+g==", + "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": { - "basic-auth": "~2.0.1", - "debug": "2.6.9", - "depd": "~2.0.0", - "on-finished": "~2.4.1", - "on-headers": "~1.1.0" + "p-limit": "^3.0.2" }, "engines": { - "node": ">= 0.8.0" + "node": ">=10" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/morgan/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "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": { - "ms": "2.0.0" - } - }, - "node_modules/morgan/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "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/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", + "callsites": "^3.0.0" + }, "engines": { - "node": ">= 0.6" + "node": ">=6" } }, - "node_modules/nocache": { + "node_modules/path-exists": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/nocache/-/nocache-4.0.0.tgz", - "integrity": "sha512-AntnTbmKZvNYIsTVPPwv7dfZdAfo/6H/2ZlZACK66NAOQtIApxkB/6pf/c+s+ACW8vemGJzUCyVTssrzNUK6yQ==", + "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": ">=16.0.0" + "node": ">=8" } }, - "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" - } - ], + "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": ">=10.5.0" + "node": ">=8" } }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "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", - "optional": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" + "node": ">=8.6" }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/nodemon": { - "version": "3.1.14", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", - "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", - "dev": true, - "license": "MIT", + "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": { - "chokidar": "^3.5.2", - "debug": "^4", - "ignore-by-default": "^1.0.1", - "minimatch": "^10.2.1", - "pstree.remy": "^1.1.8", - "semver": "^7.5.3", - "simple-update-notifier": "^2.0.0", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.5" + "playwright-core": "1.64.1" }, "bin": { - "nodemon": "bin/nodemon.js" + "playwright": "cli.js" }, "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nodemon" - } - }, - "node_modules/nodemon/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" + "node": ">=20" } }, - "node_modules/nodemon/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" + "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": ">=4" + "node": ">=20" } }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "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.10.0" + "node": ">= 0.4" } }, - "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==", + "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": ">=0.10.0" + "node": "^10 || ^12 || >=14" } }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "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", - "optional": true, "engines": { - "node": ">= 6" + "node": ">= 0.8.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==", + "node_modules/prettier": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.10.tgz", + "integrity": "sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==", + "dev": true, "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, "engines": { - "node": ">= 0.4" + "node": ">=14" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "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": { - "ee-first": "1.1.1" - }, + "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": ">= 0.8" + "node": ">=10" } }, - "node_modules/on-headers": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", - "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "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": ">= 0.8" + "node": ">=6" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "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", - "dependencies": { - "wrappy": "1" + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "license": "MIT", - "optional": true, + "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": { - "yocto-queue": "^0.1.0" + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" }, "engines": { - "node": ">=10" + "node": ">=0.6" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "license": "BlueOak-1.0.0" - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/path-expression-matcher": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz", - "integrity": "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==", + "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/NaturalIntelligence" + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" } ], - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14.0.0" + "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/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "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": ">=8" + "node": ">=0.10.0" } }, - "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==", - "license": "MIT" - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "license": "BlueOak-1.0.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": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" + "scheduler": "^0.28.0" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-to-regexp": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", - "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "peerDependencies": { + "react": "^19.3.1" } }, - "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==", + "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/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, + "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": ">=8.6" + "node": ">=10" }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "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/promise": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "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": { - "asap": "~2.0.3" + "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/proto3-json-serializer": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-4.0.2.tgz", - "integrity": "sha512-VUQlb/J2WjOG8BKhdLwJUz+MKxSMtBGYh+TaNmRtiYVhlIzXjzeAGIx7gtc3lXlA2iURzfF2+cXaQvC2ofwzpg==", - "license": "Apache-2.0", - "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": { - "protobufjs": "^7.5.4" + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" }, "engines": { - "node": ">=22" + "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/protobufjs": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz", - "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "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": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.5", - "@protobufjs/eventemitter": "^1.1.1", - "@protobufjs/fetch": "^1.1.1", - "@protobufjs/float": "^1.0.2", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.1", - "@types/node": ">=13.7.0", - "long": "^5.3.2" + "@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": ">=12.0.0" + "node": ">= 20.0.0" } }, - "node_modules/proxy-addr": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", - "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", + "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": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" + "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.10" + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "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" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/pstree.remy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", - "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "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" - }, - "node_modules/pug": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pug/-/pug-3.0.4.tgz", - "integrity": "sha512-kFfq5mMzrS7+wrl5pLJzZEzemx34OQ0w4SARfhy/3yxTlhbstsudDwJzhf1hP02yHzbjoVMSXUj/Sz6RNfMyXg==", "license": "MIT", "dependencies": { - "pug-code-gen": "^3.0.4", - "pug-filters": "^4.0.0", - "pug-lexer": "^5.0.1", - "pug-linker": "^4.0.0", - "pug-load": "^3.0.0", - "pug-parser": "^6.0.0", - "pug-runtime": "^3.0.1", - "pug-strip-comments": "^2.0.0" + "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/pug-attrs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-3.0.0.tgz", - "integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==", + "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": { - "constantinople": "^4.0.1", - "js-stringify": "^1.0.2", - "pug-runtime": "^3.0.0" + "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/pug-code-gen": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-3.0.4.tgz", - "integrity": "sha512-6okWYIKdasTyXICyEtvobmTZAVX57JkzgzIi4iRJlin8kmhG+Xry2dsus+Mun/nGCn6F2U49haHI5mkELXB14g==", + "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", - "dependencies": { - "constantinople": "^4.0.1", - "doctypes": "^1.1.0", - "js-stringify": "^1.0.2", - "pug-attrs": "^3.0.0", - "pug-error": "^2.1.0", - "pug-runtime": "^3.0.1", - "void-elements": "^3.1.0", - "with": "^7.0.0" + "engines": { + "node": ">=4" } }, - "node_modules/pug-error": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.1.0.tgz", - "integrity": "sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==", - "license": "MIT" - }, - "node_modules/pug-filters": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-4.0.0.tgz", - "integrity": "sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==", + "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", - "dependencies": { - "constantinople": "^4.0.1", - "jstransformer": "1.0.0", - "pug-error": "^2.0.0", - "pug-walk": "^2.0.0", - "resolve": "^1.15.1" + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/pug-lexer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-5.0.1.tgz", - "integrity": "sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==", + "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", - "dependencies": { - "character-parser": "^2.2.0", - "is-expression": "^4.0.0", - "pug-error": "^2.0.0" + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" } }, - "node_modules/pug-linker": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-4.0.0.tgz", - "integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==", + "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": { - "pug-error": "^2.0.0", - "pug-walk": "^2.0.0" - } - }, - "node_modules/pug-load": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-3.0.0.tgz", - "integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==", + "@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": { - "object-assign": "^4.1.1", - "pug-walk": "^2.0.0" + "queue-microtask": "^1.2.2" } }, - "node_modules/pug-parser": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-6.0.0.tgz", - "integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==", + "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": { - "pug-error": "^2.0.0", - "token-stream": "1.0.0" + "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/pug-runtime": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-3.0.1.tgz", - "integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==", + "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/pug-strip-comments": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz", - "integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==", + "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": { - "pug-error": "^2.0.0" + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/pug-walk": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-2.0.0.tgz", - "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==", - "license": "MIT" - }, - "node_modules/qs": { - "version": "6.16.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", - "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", - "license": "BSD-3-Clause", + "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": { - "es-define-property": "^1.0.1", - "side-channel": "^1.1.1" + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" }, "engines": { - "node": ">=0.6" + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/random-bytes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", - "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", - "license": "MIT", + "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": ">= 0.8" + "node": ">=10" } }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "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.6" + "node": ">= 0.4" } }, - "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "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": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" + "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.10" + "node": ">= 0.4" } }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "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": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" }, "engines": { - "node": ">= 6" + "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/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "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": { - "picomatch": "^2.2.1" + "shebang-regex": "^3.0.0" }, "engines": { - "node": ">=8.10.0" + "node": ">=8" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "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", - "optional": true, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "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": { - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" + "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" @@ -3734,240 +9174,274 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/retry-request": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-9.0.1.tgz", - "integrity": "sha512-4kGHEBl0ME6gR+8QB1sPMyg58jUxLUcCZNeqLDrfqzk0eWQN8XJFmeu7fmZCjlZCRW0S2u64Ik1HkO/0sWgCYQ==", + "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", - "optional": true, "dependencies": { - "extend": "^3.0.2", - "teeny-request": "^11.0.0" + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" }, "engines": { - "node": ">=22" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/rimraf": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", - "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", - "license": "ISC", - "optional": true, + "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": { - "glob": "^10.3.7" + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" }, - "bin": { - "rimraf": "dist/esm/bin.mjs" + "engines": { + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "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": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" + "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": ">= 18" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/router/node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" + "node_modules/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/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" - } - ], + "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/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "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/sax": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", - "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", - "license": "BlueOak-1.0.0", + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, "engines": { - "node": ">=11.0.0" + "node": ">= 0.4" } }, - "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "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": ">=10" + "node": ">= 0.4" } }, - "node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "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": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" + "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": ">= 18" + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/send/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "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", - "engines": { - "node": ">= 0.6" + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" } }, - "node_modules/send/node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "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": { - "mime-db": "^1.54.0" + "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": ">=18" + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "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": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.2" }, "engines": { - "node": ">= 18" + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/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==", + "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": { - "shebang-regex": "^3.0.0" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/shebang-regex": { + "node_modules/strip-bom": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "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/side-channel": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", - "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "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": { - "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" + "client-only": "0.0.1" }, "engines": { - "node": ">= 0.4" + "node": ">= 12.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "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/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==", + "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": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" + "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" }, @@ -3975,517 +9449,961 @@ "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==", + "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", - "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" + "node": ">=6" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, - "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==", + "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": { - "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" + "fdir": "^6.5.0", + "picomatch": "^4.0.4" }, "engines": { - "node": ">= 0.4" + "node": ">=12.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", + "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": ">=14" + "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/isaacs" + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "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": { - "semver": "^7.5.3" + "is-number": "^7.0.0" }, "engines": { - "node": ">=10" + "node": ">=8.0" } }, - "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==", + "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", - "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": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" } }, - "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==", + "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": { - "debug": "~4.4.1", - "ws": "~8.21.0" + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" } }, - "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==", + "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": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.4.1" + "minimist": "^1.2.0" }, - "engines": { - "node": ">=10.0.0" + "bin": { + "json5": "lib/cli.js" } }, - "node_modules/source-map": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", - "license": "BSD-3-Clause", - "engines": { - "node": ">= 12" - } + "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/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "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", - "engines": { - "node": ">= 0.8" + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" } }, - "node_modules/stream-events": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", - "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "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", - "optional": true, "dependencies": { - "stubs": "^3.0.0" + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" } }, - "node_modules/stream-shift": { + "node_modules/typed-array-buffer": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", - "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, "license": "MIT", - "optional": true + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "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", - "optional": true, "dependencies": { - "safe-buffer": "~5.2.0" + "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/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "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": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "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": ">=12" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "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": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "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": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", + "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": ">=8" + "node": ">=14.17" } }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "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": { - "ansi-regex": "^5.0.1" + "@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": ">=8" + "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/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "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": { - "ansi-regex": "^6.0.1" + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" }, "engines": { - "node": ">=12" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "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==", + "dev": true, + "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": { - "ansi-regex": "^5.0.1" + "napi-postinstall": "^0.3.4" }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strnum": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.2.tgz", - "integrity": "sha512-rDG3Ah4TV0k1hWvLSzkZtMmLN9+eS+h3knq4MP6A42Y3Yh5qGNnOUs1jJkoSr8FG5dsL28c7KgkIBzSEykqtuw==", + "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/NaturalIntelligence" + "url": "https://github.com/sponsors/ai" } ], "license": "MIT", - "optional": true, "dependencies": { - "anynum": "^1.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" } }, - "node_modules/stubs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", - "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", - "license": "MIT", - "optional": true + "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/stylus": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.64.0.tgz", - "integrity": "sha512-ZIdT8eUv8tegmqy1tTIdJv9We2DumkNZFdCF5mz/Kpq3OcTaxSuCAYZge6HKK2CmNC02G1eJig2RV7XTw5hQrA==", + "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": { - "@adobe/css-tools": "~4.3.3", - "debug": "^4.3.2", - "glob": "^10.4.5", - "sax": "~1.4.1", - "source-map": "^0.7.3" - }, - "bin": { - "stylus": "bin/stylus" + "tslib": "^2.0.0" }, "engines": { - "node": ">=16" + "node": ">=10" }, - "funding": { - "url": "https://opencollective.com/stylus" + "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/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==", + "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": { - "has-flag": "^4.0.0" + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" }, "engines": { - "node": ">=8" + "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/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==", + "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": ">= 0.4" + "node": "^20.19.0 || >=22.12.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "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/teeny-request": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-11.0.1.tgz", - "integrity": "sha512-bNr5j2YjSdajgCVsp+8JVjRf1uHIbpNISLeKp/7V1NnVMX3Gh6H/kECNGlm2Q3I68ACODQ0iv6aZ3Rvm8WgLGA==", - "license": "Apache-2.0", - "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": { - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.1", - "node-fetch": "^3.3.2", - "stream-events": "^1.0.5" + "detect-libc": "^2.0.3" }, "engines": { - "node": ">=22" + "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/teeny-request/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", + "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, - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, + "os": [ + "darwin" + ], + "peer": true, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">= 12.0.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/node-fetch" + "url": "https://opencollective.com/parcel" } }, - "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==", + "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": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, "engines": { - "node": ">=8.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", + "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": ">=0.6" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/token-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz", - "integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==", - "license": "MIT" - }, - "node_modules/touch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", - "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "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": "ISC", - "bin": { - "nodetouch": "bin/nodetouch.js" + "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/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT", - "optional": true + "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/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/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/type-is": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", - "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", - "license": "MIT", - "dependencies": { - "content-type": "^2.0.0", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" + "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": ">= 18" + "node": ">= 12.0.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://opencollective.com/parcel" } }, - "node_modules/type-is/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", + "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": ">= 0.6" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/type-is/node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, + "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": ">=18" + "node": ">= 12.0.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://opencollective.com/parcel" } }, - "node_modules/uid-safe": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", - "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "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", - "dependencies": { - "random-bytes": "~1.0.0" - }, + "peer": true, "engines": { - "node": ">= 0.8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/undefsafe": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", - "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "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, - "license": "MIT" - }, - "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "license": "MIT" - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "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": ">= 0.8" + "node": "^10 || ^12 || >=14" } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT", - "optional": true - }, - "node_modules/uuid": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.3.tgz", - "integrity": "sha512-Sc5u1VVln93y1Xnz4R+CunOZc7R9LUcTmhj7Rc7Eg3HVcf7rVn2FOoa812fe0W8p3jD7xeoGSYbfqCGGH2B6xw==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], + "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": { - "uuid": "dist-node/bin/uuid" + "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/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "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", - "engines": { - "node": ">= 0.8" + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" } }, - "node_modules/void-elements": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", - "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "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": ">=0.10.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/web-streams-polyfill": { @@ -4497,51 +10415,11 @@ "node": ">= 8" } }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause", - "optional": true - }, - "node_modules/websocket-driver": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.5.tgz", - "integrity": "sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA==", - "license": "Apache-2.0", - "dependencies": { - "http-parser-js": ">=0.5.1", - "safe-buffer": ">=5.1.0", - "websocket-extensions": ">=0.1.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/websocket-extensions": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", - "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "optional": true, - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -4553,235 +10431,124 @@ "node": ">= 8" } }, - "node_modules/with": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz", - "integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.9.6", - "@babel/types": "^7.9.6", - "assert-never": "^1.2.1", - "babel-walk": "3.0.0-canary-5" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "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": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" + "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": ">=12" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "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": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "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": ">=10" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "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": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "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": { - "ansi-regex": "^5.0.1" + "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": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/ws": { - "version": "8.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/xml-naming": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.3.0.tgz", - "integrity": "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "optional": true, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "license": "ISC", - "optional": true, - "engines": { - "node": ">=10" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/yargs": { - "version": "17.7.3", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", - "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "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", - "optional": true, - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "bin": { + "why-is-node-running": "cli.js" }, "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "license": "ISC", - "optional": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" + "node": ">=20.11" } }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT", - "optional": true - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "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", - "optional": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "optional": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } + "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", @@ -4799,14 +10566,37 @@ "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", - "optional": true, "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/package.json b/package.json index 5a6c667..1577c82 100644 --- a/package.json +++ b/package.json @@ -1,44 +1,64 @@ { - "name": "ipification-demo", + "name": "ipification-showcase-next", "version": "1.2.17-SNAPSHOT", - "description": "Ipification Demo", - "private": false, + "private": true, + "type": "module", "engines": { - "node": ">=24.0.0" + "node": "24.22.0" }, "scripts": { - "version": "echo $npm_package_version", - "test": "node --test", - "start": "node ./bin/www", - "dev": "nodemon ./bin/www" + "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": { - "axios": "^1.20.0", - "cache-manager": "^7.2.9", + "@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", + "axios": "^1.21.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cn": "^0.5.0", "config": "^5.0.1", - "cookie-parser": "^1.4.7", - "debug": "^4.4.3", - "dotenv": "^18.0.8", - "express": "^5.3.0", - "express-rate-limit": "^8.7.1", - "express-session": "^1.19.0", - "firebase-admin": "^14.5.0", "geoip-lite": "^2.0.3", "google-auth-library": "^11.3.1", - "helmet": "^8.3.0", - "html-entities": "^2.6.0", - "http-errors": "^2.0.1", - "json-pretty-html": "^1.1.6", + "intl-tel-input": "^29.5.7", "jsonwebtoken": "^9.0.3", - "keyv": "^6.1.0", - "morgan": "^1.12.1", - "nocache": "^4.0.0", - "pug": "^3.0.4", - "socket.io": "^4.8.4", - "stylus": "^0.64.0", - "uuid": "^14.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", + "tailwind-merge": "^3.7.0" }, "devDependencies": { - "nodemon": "^3.1.14" + "@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", + "prettier": "^3.9.10", + "tailwindcss": "^4.3.3", + "tw-animate-css": "^1.4.0", + "typescript": "^6.0.3", + "vitest": "^5.0.5" } } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..642eb3d --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,18 @@ +import { defineConfig, devices } from "@playwright/test"; +const port = process.env.E2E_PORT || "3002"; +export default defineConfig({ + testDir: "./e2e", + fullyParallel: false, + workers: 1, + use: { baseURL: `http://127.0.0.1:${port}`, trace: "retain-on-failure" }, + projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }], + 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/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 0000000..d062fcd --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,2 @@ +const config = { plugins: { "@tailwindcss/postcss": {} } }; +export default config; diff --git a/public/css/animate.css b/public/css/animate.css deleted file mode 100644 index e3582a5..0000000 --- a/public/css/animate.css +++ /dev/null @@ -1,3625 +0,0 @@ -@charset "UTF-8"; - -/*! - * animate.css -https://daneden.github.io/animate.css/ - * Version - 3.7.2 - * Licensed under the MIT license - http://opensource.org/licenses/MIT - * - * Copyright (c) 2019 Daniel Eden - */ - -@-webkit-keyframes bounce { - from, - 20%, - 53%, - 80%, - to { - -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } - - 40%, - 43% { - -webkit-animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); - animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); - -webkit-transform: translate3d(0, -30px, 0); - transform: translate3d(0, -30px, 0); - } - - 70% { - -webkit-animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); - animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); - -webkit-transform: translate3d(0, -15px, 0); - transform: translate3d(0, -15px, 0); - } - - 90% { - -webkit-transform: translate3d(0, -4px, 0); - transform: translate3d(0, -4px, 0); - } -} - -@keyframes bounce { - from, - 20%, - 53%, - 80%, - to { - -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } - - 40%, - 43% { - -webkit-animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); - animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); - -webkit-transform: translate3d(0, -30px, 0); - transform: translate3d(0, -30px, 0); - } - - 70% { - -webkit-animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); - animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); - -webkit-transform: translate3d(0, -15px, 0); - transform: translate3d(0, -15px, 0); - } - - 90% { - -webkit-transform: translate3d(0, -4px, 0); - transform: translate3d(0, -4px, 0); - } -} - -.bounce { - -webkit-animation-name: bounce; - animation-name: bounce; - -webkit-transform-origin: center bottom; - transform-origin: center bottom; -} - -@-webkit-keyframes flash { - from, - 50%, - to { - opacity: 1; - } - - 25%, - 75% { - opacity: 0; - } -} - -@keyframes flash { - from, - 50%, - to { - opacity: 1; - } - - 25%, - 75% { - opacity: 0; - } -} - -.flash { - -webkit-animation-name: flash; - animation-name: flash; -} - -/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */ - -@-webkit-keyframes pulse { - from { - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - } - - 50% { - -webkit-transform: scale3d(1.05, 1.05, 1.05); - transform: scale3d(1.05, 1.05, 1.05); - } - - to { - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - } -} - -@keyframes pulse { - from { - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - } - - 50% { - -webkit-transform: scale3d(1.05, 1.05, 1.05); - transform: scale3d(1.05, 1.05, 1.05); - } - - to { - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - } -} - -.pulse { - -webkit-animation-name: pulse; - animation-name: pulse; -} - -@-webkit-keyframes rubberBand { - from { - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - } - - 30% { - -webkit-transform: scale3d(1.25, 0.75, 1); - transform: scale3d(1.25, 0.75, 1); - } - - 40% { - -webkit-transform: scale3d(0.75, 1.25, 1); - transform: scale3d(0.75, 1.25, 1); - } - - 50% { - -webkit-transform: scale3d(1.15, 0.85, 1); - transform: scale3d(1.15, 0.85, 1); - } - - 65% { - -webkit-transform: scale3d(0.95, 1.05, 1); - transform: scale3d(0.95, 1.05, 1); - } - - 75% { - -webkit-transform: scale3d(1.05, 0.95, 1); - transform: scale3d(1.05, 0.95, 1); - } - - to { - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - } -} - -@keyframes rubberBand { - from { - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - } - - 30% { - -webkit-transform: scale3d(1.25, 0.75, 1); - transform: scale3d(1.25, 0.75, 1); - } - - 40% { - -webkit-transform: scale3d(0.75, 1.25, 1); - transform: scale3d(0.75, 1.25, 1); - } - - 50% { - -webkit-transform: scale3d(1.15, 0.85, 1); - transform: scale3d(1.15, 0.85, 1); - } - - 65% { - -webkit-transform: scale3d(0.95, 1.05, 1); - transform: scale3d(0.95, 1.05, 1); - } - - 75% { - -webkit-transform: scale3d(1.05, 0.95, 1); - transform: scale3d(1.05, 0.95, 1); - } - - to { - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - } -} - -.rubberBand { - -webkit-animation-name: rubberBand; - animation-name: rubberBand; -} - -@-webkit-keyframes shake { - from, - to { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } - - 10%, - 30%, - 50%, - 70%, - 90% { - -webkit-transform: translate3d(-10px, 0, 0); - transform: translate3d(-10px, 0, 0); - } - - 20%, - 40%, - 60%, - 80% { - -webkit-transform: translate3d(10px, 0, 0); - transform: translate3d(10px, 0, 0); - } -} - -@keyframes shake { - from, - to { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } - - 10%, - 30%, - 50%, - 70%, - 90% { - -webkit-transform: translate3d(-10px, 0, 0); - transform: translate3d(-10px, 0, 0); - } - - 20%, - 40%, - 60%, - 80% { - -webkit-transform: translate3d(10px, 0, 0); - transform: translate3d(10px, 0, 0); - } -} - -.shake { - -webkit-animation-name: shake; - animation-name: shake; -} - -@-webkit-keyframes headShake { - 0% { - -webkit-transform: translateX(0); - transform: translateX(0); - } - - 6.5% { - -webkit-transform: translateX(-6px) rotateY(-9deg); - transform: translateX(-6px) rotateY(-9deg); - } - - 18.5% { - -webkit-transform: translateX(5px) rotateY(7deg); - transform: translateX(5px) rotateY(7deg); - } - - 31.5% { - -webkit-transform: translateX(-3px) rotateY(-5deg); - transform: translateX(-3px) rotateY(-5deg); - } - - 43.5% { - -webkit-transform: translateX(2px) rotateY(3deg); - transform: translateX(2px) rotateY(3deg); - } - - 50% { - -webkit-transform: translateX(0); - transform: translateX(0); - } -} - -@keyframes headShake { - 0% { - -webkit-transform: translateX(0); - transform: translateX(0); - } - - 6.5% { - -webkit-transform: translateX(-6px) rotateY(-9deg); - transform: translateX(-6px) rotateY(-9deg); - } - - 18.5% { - -webkit-transform: translateX(5px) rotateY(7deg); - transform: translateX(5px) rotateY(7deg); - } - - 31.5% { - -webkit-transform: translateX(-3px) rotateY(-5deg); - transform: translateX(-3px) rotateY(-5deg); - } - - 43.5% { - -webkit-transform: translateX(2px) rotateY(3deg); - transform: translateX(2px) rotateY(3deg); - } - - 50% { - -webkit-transform: translateX(0); - transform: translateX(0); - } -} - -.headShake { - -webkit-animation-timing-function: ease-in-out; - animation-timing-function: ease-in-out; - -webkit-animation-name: headShake; - animation-name: headShake; -} - -@-webkit-keyframes swing { - 20% { - -webkit-transform: rotate3d(0, 0, 1, 15deg); - transform: rotate3d(0, 0, 1, 15deg); - } - - 40% { - -webkit-transform: rotate3d(0, 0, 1, -10deg); - transform: rotate3d(0, 0, 1, -10deg); - } - - 60% { - -webkit-transform: rotate3d(0, 0, 1, 5deg); - transform: rotate3d(0, 0, 1, 5deg); - } - - 80% { - -webkit-transform: rotate3d(0, 0, 1, -5deg); - transform: rotate3d(0, 0, 1, -5deg); - } - - to { - -webkit-transform: rotate3d(0, 0, 1, 0deg); - transform: rotate3d(0, 0, 1, 0deg); - } -} - -@keyframes swing { - 20% { - -webkit-transform: rotate3d(0, 0, 1, 15deg); - transform: rotate3d(0, 0, 1, 15deg); - } - - 40% { - -webkit-transform: rotate3d(0, 0, 1, -10deg); - transform: rotate3d(0, 0, 1, -10deg); - } - - 60% { - -webkit-transform: rotate3d(0, 0, 1, 5deg); - transform: rotate3d(0, 0, 1, 5deg); - } - - 80% { - -webkit-transform: rotate3d(0, 0, 1, -5deg); - transform: rotate3d(0, 0, 1, -5deg); - } - - to { - -webkit-transform: rotate3d(0, 0, 1, 0deg); - transform: rotate3d(0, 0, 1, 0deg); - } -} - -.swing { - -webkit-transform-origin: top center; - transform-origin: top center; - -webkit-animation-name: swing; - animation-name: swing; -} - -@-webkit-keyframes tada { - from { - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - } - - 10%, - 20% { - -webkit-transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); - transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); - } - - 30%, - 50%, - 70%, - 90% { - -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); - transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); - } - - 40%, - 60%, - 80% { - -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); - transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); - } - - to { - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - } -} - -@keyframes tada { - from { - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - } - - 10%, - 20% { - -webkit-transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); - transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); - } - - 30%, - 50%, - 70%, - 90% { - -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); - transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); - } - - 40%, - 60%, - 80% { - -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); - transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); - } - - to { - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - } -} - -.tada { - -webkit-animation-name: tada; - animation-name: tada; -} - -/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */ - -@-webkit-keyframes wobble { - from { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } - - 15% { - -webkit-transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg); - transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg); - } - - 30% { - -webkit-transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg); - transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg); - } - - 45% { - -webkit-transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg); - transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg); - } - - 60% { - -webkit-transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg); - transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg); - } - - 75% { - -webkit-transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg); - transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg); - } - - to { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -@keyframes wobble { - from { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } - - 15% { - -webkit-transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg); - transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg); - } - - 30% { - -webkit-transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg); - transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg); - } - - 45% { - -webkit-transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg); - transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg); - } - - 60% { - -webkit-transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg); - transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg); - } - - 75% { - -webkit-transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg); - transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg); - } - - to { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -.wobble { - -webkit-animation-name: wobble; - animation-name: wobble; -} - -@-webkit-keyframes jello { - from, - 11.1%, - to { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } - - 22.2% { - -webkit-transform: skewX(-12.5deg) skewY(-12.5deg); - transform: skewX(-12.5deg) skewY(-12.5deg); - } - - 33.3% { - -webkit-transform: skewX(6.25deg) skewY(6.25deg); - transform: skewX(6.25deg) skewY(6.25deg); - } - - 44.4% { - -webkit-transform: skewX(-3.125deg) skewY(-3.125deg); - transform: skewX(-3.125deg) skewY(-3.125deg); - } - - 55.5% { - -webkit-transform: skewX(1.5625deg) skewY(1.5625deg); - transform: skewX(1.5625deg) skewY(1.5625deg); - } - - 66.6% { - -webkit-transform: skewX(-0.78125deg) skewY(-0.78125deg); - transform: skewX(-0.78125deg) skewY(-0.78125deg); - } - - 77.7% { - -webkit-transform: skewX(0.390625deg) skewY(0.390625deg); - transform: skewX(0.390625deg) skewY(0.390625deg); - } - - 88.8% { - -webkit-transform: skewX(-0.1953125deg) skewY(-0.1953125deg); - transform: skewX(-0.1953125deg) skewY(-0.1953125deg); - } -} - -@keyframes jello { - from, - 11.1%, - to { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } - - 22.2% { - -webkit-transform: skewX(-12.5deg) skewY(-12.5deg); - transform: skewX(-12.5deg) skewY(-12.5deg); - } - - 33.3% { - -webkit-transform: skewX(6.25deg) skewY(6.25deg); - transform: skewX(6.25deg) skewY(6.25deg); - } - - 44.4% { - -webkit-transform: skewX(-3.125deg) skewY(-3.125deg); - transform: skewX(-3.125deg) skewY(-3.125deg); - } - - 55.5% { - -webkit-transform: skewX(1.5625deg) skewY(1.5625deg); - transform: skewX(1.5625deg) skewY(1.5625deg); - } - - 66.6% { - -webkit-transform: skewX(-0.78125deg) skewY(-0.78125deg); - transform: skewX(-0.78125deg) skewY(-0.78125deg); - } - - 77.7% { - -webkit-transform: skewX(0.390625deg) skewY(0.390625deg); - transform: skewX(0.390625deg) skewY(0.390625deg); - } - - 88.8% { - -webkit-transform: skewX(-0.1953125deg) skewY(-0.1953125deg); - transform: skewX(-0.1953125deg) skewY(-0.1953125deg); - } -} - -.jello { - -webkit-animation-name: jello; - animation-name: jello; - -webkit-transform-origin: center; - transform-origin: center; -} - -@-webkit-keyframes heartBeat { - 0% { - -webkit-transform: scale(1); - transform: scale(1); - } - - 14% { - -webkit-transform: scale(1.3); - transform: scale(1.3); - } - - 28% { - -webkit-transform: scale(1); - transform: scale(1); - } - - 42% { - -webkit-transform: scale(1.3); - transform: scale(1.3); - } - - 70% { - -webkit-transform: scale(1); - transform: scale(1); - } -} - -@keyframes heartBeat { - 0% { - -webkit-transform: scale(1); - transform: scale(1); - } - - 14% { - -webkit-transform: scale(1.3); - transform: scale(1.3); - } - - 28% { - -webkit-transform: scale(1); - transform: scale(1); - } - - 42% { - -webkit-transform: scale(1.3); - transform: scale(1.3); - } - - 70% { - -webkit-transform: scale(1); - transform: scale(1); - } -} - -.heartBeat { - -webkit-animation-name: heartBeat; - animation-name: heartBeat; - -webkit-animation-duration: 1.3s; - animation-duration: 1.3s; - -webkit-animation-timing-function: ease-in-out; - animation-timing-function: ease-in-out; -} - -@-webkit-keyframes bounceIn { - from, - 20%, - 40%, - 60%, - 80%, - to { - -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - } - - 0% { - opacity: 0; - -webkit-transform: scale3d(0.3, 0.3, 0.3); - transform: scale3d(0.3, 0.3, 0.3); - } - - 20% { - -webkit-transform: scale3d(1.1, 1.1, 1.1); - transform: scale3d(1.1, 1.1, 1.1); - } - - 40% { - -webkit-transform: scale3d(0.9, 0.9, 0.9); - transform: scale3d(0.9, 0.9, 0.9); - } - - 60% { - opacity: 1; - -webkit-transform: scale3d(1.03, 1.03, 1.03); - transform: scale3d(1.03, 1.03, 1.03); - } - - 80% { - -webkit-transform: scale3d(0.97, 0.97, 0.97); - transform: scale3d(0.97, 0.97, 0.97); - } - - to { - opacity: 1; - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - } -} - -@keyframes bounceIn { - from, - 20%, - 40%, - 60%, - 80%, - to { - -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - } - - 0% { - opacity: 0; - -webkit-transform: scale3d(0.3, 0.3, 0.3); - transform: scale3d(0.3, 0.3, 0.3); - } - - 20% { - -webkit-transform: scale3d(1.1, 1.1, 1.1); - transform: scale3d(1.1, 1.1, 1.1); - } - - 40% { - -webkit-transform: scale3d(0.9, 0.9, 0.9); - transform: scale3d(0.9, 0.9, 0.9); - } - - 60% { - opacity: 1; - -webkit-transform: scale3d(1.03, 1.03, 1.03); - transform: scale3d(1.03, 1.03, 1.03); - } - - 80% { - -webkit-transform: scale3d(0.97, 0.97, 0.97); - transform: scale3d(0.97, 0.97, 0.97); - } - - to { - opacity: 1; - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - } -} - -.bounceIn { - -webkit-animation-duration: 0.75s; - animation-duration: 0.75s; - -webkit-animation-name: bounceIn; - animation-name: bounceIn; -} - -@-webkit-keyframes bounceInDown { - from, - 60%, - 75%, - 90%, - to { - -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - } - - 0% { - opacity: 0; - -webkit-transform: translate3d(0, -3000px, 0); - transform: translate3d(0, -3000px, 0); - } - - 60% { - opacity: 1; - -webkit-transform: translate3d(0, 25px, 0); - transform: translate3d(0, 25px, 0); - } - - 75% { - -webkit-transform: translate3d(0, -10px, 0); - transform: translate3d(0, -10px, 0); - } - - 90% { - -webkit-transform: translate3d(0, 5px, 0); - transform: translate3d(0, 5px, 0); - } - - to { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -@keyframes bounceInDown { - from, - 60%, - 75%, - 90%, - to { - -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - } - - 0% { - opacity: 0; - -webkit-transform: translate3d(0, -3000px, 0); - transform: translate3d(0, -3000px, 0); - } - - 60% { - opacity: 1; - -webkit-transform: translate3d(0, 25px, 0); - transform: translate3d(0, 25px, 0); - } - - 75% { - -webkit-transform: translate3d(0, -10px, 0); - transform: translate3d(0, -10px, 0); - } - - 90% { - -webkit-transform: translate3d(0, 5px, 0); - transform: translate3d(0, 5px, 0); - } - - to { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -.bounceInDown { - -webkit-animation-name: bounceInDown; - animation-name: bounceInDown; -} - -@-webkit-keyframes bounceInLeft { - from, - 60%, - 75%, - 90%, - to { - -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - } - - 0% { - opacity: 0; - -webkit-transform: translate3d(-3000px, 0, 0); - transform: translate3d(-3000px, 0, 0); - } - - 60% { - opacity: 1; - -webkit-transform: translate3d(25px, 0, 0); - transform: translate3d(25px, 0, 0); - } - - 75% { - -webkit-transform: translate3d(-10px, 0, 0); - transform: translate3d(-10px, 0, 0); - } - - 90% { - -webkit-transform: translate3d(5px, 0, 0); - transform: translate3d(5px, 0, 0); - } - - to { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -@keyframes bounceInLeft { - from, - 60%, - 75%, - 90%, - to { - -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - } - - 0% { - opacity: 0; - -webkit-transform: translate3d(-3000px, 0, 0); - transform: translate3d(-3000px, 0, 0); - } - - 60% { - opacity: 1; - -webkit-transform: translate3d(25px, 0, 0); - transform: translate3d(25px, 0, 0); - } - - 75% { - -webkit-transform: translate3d(-10px, 0, 0); - transform: translate3d(-10px, 0, 0); - } - - 90% { - -webkit-transform: translate3d(5px, 0, 0); - transform: translate3d(5px, 0, 0); - } - - to { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -.bounceInLeft { - -webkit-animation-name: bounceInLeft; - animation-name: bounceInLeft; -} - -@-webkit-keyframes bounceInRight { - from, - 60%, - 75%, - 90%, - to { - -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - } - - from { - opacity: 0; - -webkit-transform: translate3d(3000px, 0, 0); - transform: translate3d(3000px, 0, 0); - } - - 60% { - opacity: 1; - -webkit-transform: translate3d(-25px, 0, 0); - transform: translate3d(-25px, 0, 0); - } - - 75% { - -webkit-transform: translate3d(10px, 0, 0); - transform: translate3d(10px, 0, 0); - } - - 90% { - -webkit-transform: translate3d(-5px, 0, 0); - transform: translate3d(-5px, 0, 0); - } - - to { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -@keyframes bounceInRight { - from, - 60%, - 75%, - 90%, - to { - -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - } - - from { - opacity: 0; - -webkit-transform: translate3d(3000px, 0, 0); - transform: translate3d(3000px, 0, 0); - } - - 60% { - opacity: 1; - -webkit-transform: translate3d(-25px, 0, 0); - transform: translate3d(-25px, 0, 0); - } - - 75% { - -webkit-transform: translate3d(10px, 0, 0); - transform: translate3d(10px, 0, 0); - } - - 90% { - -webkit-transform: translate3d(-5px, 0, 0); - transform: translate3d(-5px, 0, 0); - } - - to { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -.bounceInRight { - -webkit-animation-name: bounceInRight; - animation-name: bounceInRight; -} - -@-webkit-keyframes bounceInUp { - from, - 60%, - 75%, - 90%, - to { - -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - } - - from { - opacity: 0; - -webkit-transform: translate3d(0, 3000px, 0); - transform: translate3d(0, 3000px, 0); - } - - 60% { - opacity: 1; - -webkit-transform: translate3d(0, -20px, 0); - transform: translate3d(0, -20px, 0); - } - - 75% { - -webkit-transform: translate3d(0, 10px, 0); - transform: translate3d(0, 10px, 0); - } - - 90% { - -webkit-transform: translate3d(0, -5px, 0); - transform: translate3d(0, -5px, 0); - } - - to { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -@keyframes bounceInUp { - from, - 60%, - 75%, - 90%, - to { - -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - } - - from { - opacity: 0; - -webkit-transform: translate3d(0, 3000px, 0); - transform: translate3d(0, 3000px, 0); - } - - 60% { - opacity: 1; - -webkit-transform: translate3d(0, -20px, 0); - transform: translate3d(0, -20px, 0); - } - - 75% { - -webkit-transform: translate3d(0, 10px, 0); - transform: translate3d(0, 10px, 0); - } - - 90% { - -webkit-transform: translate3d(0, -5px, 0); - transform: translate3d(0, -5px, 0); - } - - to { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -.bounceInUp { - -webkit-animation-name: bounceInUp; - animation-name: bounceInUp; -} - -@-webkit-keyframes bounceOut { - 20% { - -webkit-transform: scale3d(0.9, 0.9, 0.9); - transform: scale3d(0.9, 0.9, 0.9); - } - - 50%, - 55% { - opacity: 1; - -webkit-transform: scale3d(1.1, 1.1, 1.1); - transform: scale3d(1.1, 1.1, 1.1); - } - - to { - opacity: 0; - -webkit-transform: scale3d(0.3, 0.3, 0.3); - transform: scale3d(0.3, 0.3, 0.3); - } -} - -@keyframes bounceOut { - 20% { - -webkit-transform: scale3d(0.9, 0.9, 0.9); - transform: scale3d(0.9, 0.9, 0.9); - } - - 50%, - 55% { - opacity: 1; - -webkit-transform: scale3d(1.1, 1.1, 1.1); - transform: scale3d(1.1, 1.1, 1.1); - } - - to { - opacity: 0; - -webkit-transform: scale3d(0.3, 0.3, 0.3); - transform: scale3d(0.3, 0.3, 0.3); - } -} - -.bounceOut { - -webkit-animation-duration: 0.75s; - animation-duration: 0.75s; - -webkit-animation-name: bounceOut; - animation-name: bounceOut; -} - -@-webkit-keyframes bounceOutDown { - 20% { - -webkit-transform: translate3d(0, 10px, 0); - transform: translate3d(0, 10px, 0); - } - - 40%, - 45% { - opacity: 1; - -webkit-transform: translate3d(0, -20px, 0); - transform: translate3d(0, -20px, 0); - } - - to { - opacity: 0; - -webkit-transform: translate3d(0, 2000px, 0); - transform: translate3d(0, 2000px, 0); - } -} - -@keyframes bounceOutDown { - 20% { - -webkit-transform: translate3d(0, 10px, 0); - transform: translate3d(0, 10px, 0); - } - - 40%, - 45% { - opacity: 1; - -webkit-transform: translate3d(0, -20px, 0); - transform: translate3d(0, -20px, 0); - } - - to { - opacity: 0; - -webkit-transform: translate3d(0, 2000px, 0); - transform: translate3d(0, 2000px, 0); - } -} - -.bounceOutDown { - -webkit-animation-name: bounceOutDown; - animation-name: bounceOutDown; -} - -@-webkit-keyframes bounceOutLeft { - 20% { - opacity: 1; - -webkit-transform: translate3d(20px, 0, 0); - transform: translate3d(20px, 0, 0); - } - - to { - opacity: 0; - -webkit-transform: translate3d(-2000px, 0, 0); - transform: translate3d(-2000px, 0, 0); - } -} - -@keyframes bounceOutLeft { - 20% { - opacity: 1; - -webkit-transform: translate3d(20px, 0, 0); - transform: translate3d(20px, 0, 0); - } - - to { - opacity: 0; - -webkit-transform: translate3d(-2000px, 0, 0); - transform: translate3d(-2000px, 0, 0); - } -} - -.bounceOutLeft { - -webkit-animation-name: bounceOutLeft; - animation-name: bounceOutLeft; -} - -@-webkit-keyframes bounceOutRight { - 20% { - opacity: 1; - -webkit-transform: translate3d(-20px, 0, 0); - transform: translate3d(-20px, 0, 0); - } - - to { - opacity: 0; - -webkit-transform: translate3d(2000px, 0, 0); - transform: translate3d(2000px, 0, 0); - } -} - -@keyframes bounceOutRight { - 20% { - opacity: 1; - -webkit-transform: translate3d(-20px, 0, 0); - transform: translate3d(-20px, 0, 0); - } - - to { - opacity: 0; - -webkit-transform: translate3d(2000px, 0, 0); - transform: translate3d(2000px, 0, 0); - } -} - -.bounceOutRight { - -webkit-animation-name: bounceOutRight; - animation-name: bounceOutRight; -} - -@-webkit-keyframes bounceOutUp { - 20% { - -webkit-transform: translate3d(0, -10px, 0); - transform: translate3d(0, -10px, 0); - } - - 40%, - 45% { - opacity: 1; - -webkit-transform: translate3d(0, 20px, 0); - transform: translate3d(0, 20px, 0); - } - - to { - opacity: 0; - -webkit-transform: translate3d(0, -2000px, 0); - transform: translate3d(0, -2000px, 0); - } -} - -@keyframes bounceOutUp { - 20% { - -webkit-transform: translate3d(0, -10px, 0); - transform: translate3d(0, -10px, 0); - } - - 40%, - 45% { - opacity: 1; - -webkit-transform: translate3d(0, 20px, 0); - transform: translate3d(0, 20px, 0); - } - - to { - opacity: 0; - -webkit-transform: translate3d(0, -2000px, 0); - transform: translate3d(0, -2000px, 0); - } -} - -.bounceOutUp { - -webkit-animation-name: bounceOutUp; - animation-name: bounceOutUp; -} - -@-webkit-keyframes fadeIn { - from { - opacity: 0; - } - - to { - opacity: 1; - } -} - -@keyframes fadeIn { - from { - opacity: 0; - } - - to { - opacity: 1; - } -} - -.fadeIn { - -webkit-animation-name: fadeIn; - animation-name: fadeIn; -} - -@-webkit-keyframes fadeInDown { - from { - opacity: 0; - -webkit-transform: translate3d(0, -100%, 0); - transform: translate3d(0, -100%, 0); - } - - to { - opacity: 1; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -@keyframes fadeInDown { - from { - opacity: 0; - -webkit-transform: translate3d(0, -100%, 0); - transform: translate3d(0, -100%, 0); - } - - to { - opacity: 1; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -.fadeInDown { - -webkit-animation-name: fadeInDown; - animation-name: fadeInDown; -} - -@-webkit-keyframes fadeInDownBig { - from { - opacity: 0; - -webkit-transform: translate3d(0, -2000px, 0); - transform: translate3d(0, -2000px, 0); - } - - to { - opacity: 1; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -@keyframes fadeInDownBig { - from { - opacity: 0; - -webkit-transform: translate3d(0, -2000px, 0); - transform: translate3d(0, -2000px, 0); - } - - to { - opacity: 1; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -.fadeInDownBig { - -webkit-animation-name: fadeInDownBig; - animation-name: fadeInDownBig; -} - -@-webkit-keyframes fadeInLeft { - from { - opacity: 0; - -webkit-transform: translate3d(-100%, 0, 0); - transform: translate3d(-100%, 0, 0); - } - - to { - opacity: 1; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -@keyframes fadeInLeft { - from { - opacity: 0; - -webkit-transform: translate3d(-100%, 0, 0); - transform: translate3d(-100%, 0, 0); - } - - to { - opacity: 1; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -.fadeInLeft { - -webkit-animation-name: fadeInLeft; - animation-name: fadeInLeft; -} - -@-webkit-keyframes fadeInLeftBig { - from { - opacity: 0; - -webkit-transform: translate3d(-2000px, 0, 0); - transform: translate3d(-2000px, 0, 0); - } - - to { - opacity: 1; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -@keyframes fadeInLeftBig { - from { - opacity: 0; - -webkit-transform: translate3d(-2000px, 0, 0); - transform: translate3d(-2000px, 0, 0); - } - - to { - opacity: 1; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -.fadeInLeftBig { - -webkit-animation-name: fadeInLeftBig; - animation-name: fadeInLeftBig; -} - -@-webkit-keyframes fadeInRight { - from { - opacity: 0; - -webkit-transform: translate3d(100%, 0, 0); - transform: translate3d(100%, 0, 0); - } - - to { - opacity: 1; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -@keyframes fadeInRight { - from { - opacity: 0; - -webkit-transform: translate3d(100%, 0, 0); - transform: translate3d(100%, 0, 0); - } - - to { - opacity: 1; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -.fadeInRight { - -webkit-animation-name: fadeInRight; - animation-name: fadeInRight; -} - -@-webkit-keyframes fadeInRightBig { - from { - opacity: 0; - -webkit-transform: translate3d(2000px, 0, 0); - transform: translate3d(2000px, 0, 0); - } - - to { - opacity: 1; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -@keyframes fadeInRightBig { - from { - opacity: 0; - -webkit-transform: translate3d(2000px, 0, 0); - transform: translate3d(2000px, 0, 0); - } - - to { - opacity: 1; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -.fadeInRightBig { - -webkit-animation-name: fadeInRightBig; - animation-name: fadeInRightBig; -} - -@-webkit-keyframes fadeInUp { - from { - opacity: 0; - -webkit-transform: translate3d(0, 100%, 0); - transform: translate3d(0, 100%, 0); - } - - to { - opacity: 1; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -@keyframes fadeInUp { - from { - opacity: 0; - -webkit-transform: translate3d(0, 100%, 0); - transform: translate3d(0, 100%, 0); - } - - to { - opacity: 1; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -.fadeInUp { - -webkit-animation-name: fadeInUp; - animation-name: fadeInUp; -} - -@-webkit-keyframes fadeInUpBig { - from { - opacity: 0; - -webkit-transform: translate3d(0, 2000px, 0); - transform: translate3d(0, 2000px, 0); - } - - to { - opacity: 1; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -@keyframes fadeInUpBig { - from { - opacity: 0; - -webkit-transform: translate3d(0, 2000px, 0); - transform: translate3d(0, 2000px, 0); - } - - to { - opacity: 1; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -.fadeInUpBig { - -webkit-animation-name: fadeInUpBig; - animation-name: fadeInUpBig; -} - -@-webkit-keyframes fadeOut { - from { - opacity: 1; - } - - to { - opacity: 0; - } -} - -@keyframes fadeOut { - from { - opacity: 1; - } - - to { - opacity: 0; - } -} - -.fadeOut { - -webkit-animation-name: fadeOut; - animation-name: fadeOut; -} - -@-webkit-keyframes fadeOutDown { - from { - opacity: 1; - } - - to { - opacity: 0; - -webkit-transform: translate3d(0, 100%, 0); - transform: translate3d(0, 100%, 0); - } -} - -@keyframes fadeOutDown { - from { - opacity: 1; - } - - to { - opacity: 0; - -webkit-transform: translate3d(0, 100%, 0); - transform: translate3d(0, 100%, 0); - } -} - -.fadeOutDown { - -webkit-animation-name: fadeOutDown; - animation-name: fadeOutDown; -} - -@-webkit-keyframes fadeOutDownBig { - from { - opacity: 1; - } - - to { - opacity: 0; - -webkit-transform: translate3d(0, 2000px, 0); - transform: translate3d(0, 2000px, 0); - } -} - -@keyframes fadeOutDownBig { - from { - opacity: 1; - } - - to { - opacity: 0; - -webkit-transform: translate3d(0, 2000px, 0); - transform: translate3d(0, 2000px, 0); - } -} - -.fadeOutDownBig { - -webkit-animation-name: fadeOutDownBig; - animation-name: fadeOutDownBig; -} - -@-webkit-keyframes fadeOutLeft { - from { - opacity: 1; - } - - to { - opacity: 0; - -webkit-transform: translate3d(-100%, 0, 0); - transform: translate3d(-100%, 0, 0); - } -} - -@keyframes fadeOutLeft { - from { - opacity: 1; - } - - to { - opacity: 0; - -webkit-transform: translate3d(-100%, 0, 0); - transform: translate3d(-100%, 0, 0); - } -} - -.fadeOutLeft { - -webkit-animation-name: fadeOutLeft; - animation-name: fadeOutLeft; -} - -@-webkit-keyframes fadeOutLeftBig { - from { - opacity: 1; - } - - to { - opacity: 0; - -webkit-transform: translate3d(-2000px, 0, 0); - transform: translate3d(-2000px, 0, 0); - } -} - -@keyframes fadeOutLeftBig { - from { - opacity: 1; - } - - to { - opacity: 0; - -webkit-transform: translate3d(-2000px, 0, 0); - transform: translate3d(-2000px, 0, 0); - } -} - -.fadeOutLeftBig { - -webkit-animation-name: fadeOutLeftBig; - animation-name: fadeOutLeftBig; -} - -@-webkit-keyframes fadeOutRight { - from { - opacity: 1; - } - - to { - opacity: 0; - -webkit-transform: translate3d(100%, 0, 0); - transform: translate3d(100%, 0, 0); - } -} - -@keyframes fadeOutRight { - from { - opacity: 1; - } - - to { - opacity: 0; - -webkit-transform: translate3d(100%, 0, 0); - transform: translate3d(100%, 0, 0); - } -} - -.fadeOutRight { - -webkit-animation-name: fadeOutRight; - animation-name: fadeOutRight; -} - -@-webkit-keyframes fadeOutRightBig { - from { - opacity: 1; - } - - to { - opacity: 0; - -webkit-transform: translate3d(2000px, 0, 0); - transform: translate3d(2000px, 0, 0); - } -} - -@keyframes fadeOutRightBig { - from { - opacity: 1; - } - - to { - opacity: 0; - -webkit-transform: translate3d(2000px, 0, 0); - transform: translate3d(2000px, 0, 0); - } -} - -.fadeOutRightBig { - -webkit-animation-name: fadeOutRightBig; - animation-name: fadeOutRightBig; -} - -@-webkit-keyframes fadeOutUp { - from { - opacity: 1; - } - - to { - opacity: 0; - -webkit-transform: translate3d(0, -100%, 0); - transform: translate3d(0, -100%, 0); - } -} - -@keyframes fadeOutUp { - from { - opacity: 1; - } - - to { - opacity: 0; - -webkit-transform: translate3d(0, -100%, 0); - transform: translate3d(0, -100%, 0); - } -} - -.fadeOutUp { - -webkit-animation-name: fadeOutUp; - animation-name: fadeOutUp; -} - -@-webkit-keyframes fadeOutUpBig { - from { - opacity: 1; - } - - to { - opacity: 0; - -webkit-transform: translate3d(0, -2000px, 0); - transform: translate3d(0, -2000px, 0); - } -} - -@keyframes fadeOutUpBig { - from { - opacity: 1; - } - - to { - opacity: 0; - -webkit-transform: translate3d(0, -2000px, 0); - transform: translate3d(0, -2000px, 0); - } -} - -.fadeOutUpBig { - -webkit-animation-name: fadeOutUpBig; - animation-name: fadeOutUpBig; -} - -@-webkit-keyframes flip { - from { - -webkit-transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 0) - rotate3d(0, 1, 0, -360deg); - transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 0) rotate3d(0, 1, 0, -360deg); - -webkit-animation-timing-function: ease-out; - animation-timing-function: ease-out; - } - - 40% { - -webkit-transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 150px) - rotate3d(0, 1, 0, -190deg); - transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 150px) - rotate3d(0, 1, 0, -190deg); - -webkit-animation-timing-function: ease-out; - animation-timing-function: ease-out; - } - - 50% { - -webkit-transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 150px) - rotate3d(0, 1, 0, -170deg); - transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 150px) - rotate3d(0, 1, 0, -170deg); - -webkit-animation-timing-function: ease-in; - animation-timing-function: ease-in; - } - - 80% { - -webkit-transform: perspective(400px) scale3d(0.95, 0.95, 0.95) translate3d(0, 0, 0) - rotate3d(0, 1, 0, 0deg); - transform: perspective(400px) scale3d(0.95, 0.95, 0.95) translate3d(0, 0, 0) - rotate3d(0, 1, 0, 0deg); - -webkit-animation-timing-function: ease-in; - animation-timing-function: ease-in; - } - - to { - -webkit-transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 0) - rotate3d(0, 1, 0, 0deg); - transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 0) rotate3d(0, 1, 0, 0deg); - -webkit-animation-timing-function: ease-in; - animation-timing-function: ease-in; - } -} - -@keyframes flip { - from { - -webkit-transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 0) - rotate3d(0, 1, 0, -360deg); - transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 0) rotate3d(0, 1, 0, -360deg); - -webkit-animation-timing-function: ease-out; - animation-timing-function: ease-out; - } - - 40% { - -webkit-transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 150px) - rotate3d(0, 1, 0, -190deg); - transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 150px) - rotate3d(0, 1, 0, -190deg); - -webkit-animation-timing-function: ease-out; - animation-timing-function: ease-out; - } - - 50% { - -webkit-transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 150px) - rotate3d(0, 1, 0, -170deg); - transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 150px) - rotate3d(0, 1, 0, -170deg); - -webkit-animation-timing-function: ease-in; - animation-timing-function: ease-in; - } - - 80% { - -webkit-transform: perspective(400px) scale3d(0.95, 0.95, 0.95) translate3d(0, 0, 0) - rotate3d(0, 1, 0, 0deg); - transform: perspective(400px) scale3d(0.95, 0.95, 0.95) translate3d(0, 0, 0) - rotate3d(0, 1, 0, 0deg); - -webkit-animation-timing-function: ease-in; - animation-timing-function: ease-in; - } - - to { - -webkit-transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 0) - rotate3d(0, 1, 0, 0deg); - transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 0) rotate3d(0, 1, 0, 0deg); - -webkit-animation-timing-function: ease-in; - animation-timing-function: ease-in; - } -} - -.animated.flip { - -webkit-backface-visibility: visible; - backface-visibility: visible; - -webkit-animation-name: flip; - animation-name: flip; -} - -@-webkit-keyframes flipInX { - from { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg); - transform: perspective(400px) rotate3d(1, 0, 0, 90deg); - -webkit-animation-timing-function: ease-in; - animation-timing-function: ease-in; - opacity: 0; - } - - 40% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg); - transform: perspective(400px) rotate3d(1, 0, 0, -20deg); - -webkit-animation-timing-function: ease-in; - animation-timing-function: ease-in; - } - - 60% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 10deg); - transform: perspective(400px) rotate3d(1, 0, 0, 10deg); - opacity: 1; - } - - 80% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -5deg); - transform: perspective(400px) rotate3d(1, 0, 0, -5deg); - } - - to { - -webkit-transform: perspective(400px); - transform: perspective(400px); - } -} - -@keyframes flipInX { - from { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg); - transform: perspective(400px) rotate3d(1, 0, 0, 90deg); - -webkit-animation-timing-function: ease-in; - animation-timing-function: ease-in; - opacity: 0; - } - - 40% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg); - transform: perspective(400px) rotate3d(1, 0, 0, -20deg); - -webkit-animation-timing-function: ease-in; - animation-timing-function: ease-in; - } - - 60% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 10deg); - transform: perspective(400px) rotate3d(1, 0, 0, 10deg); - opacity: 1; - } - - 80% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -5deg); - transform: perspective(400px) rotate3d(1, 0, 0, -5deg); - } - - to { - -webkit-transform: perspective(400px); - transform: perspective(400px); - } -} - -.flipInX { - -webkit-backface-visibility: visible !important; - backface-visibility: visible !important; - -webkit-animation-name: flipInX; - animation-name: flipInX; -} - -@-webkit-keyframes flipInY { - from { - -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg); - transform: perspective(400px) rotate3d(0, 1, 0, 90deg); - -webkit-animation-timing-function: ease-in; - animation-timing-function: ease-in; - opacity: 0; - } - - 40% { - -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -20deg); - transform: perspective(400px) rotate3d(0, 1, 0, -20deg); - -webkit-animation-timing-function: ease-in; - animation-timing-function: ease-in; - } - - 60% { - -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 10deg); - transform: perspective(400px) rotate3d(0, 1, 0, 10deg); - opacity: 1; - } - - 80% { - -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -5deg); - transform: perspective(400px) rotate3d(0, 1, 0, -5deg); - } - - to { - -webkit-transform: perspective(400px); - transform: perspective(400px); - } -} - -@keyframes flipInY { - from { - -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg); - transform: perspective(400px) rotate3d(0, 1, 0, 90deg); - -webkit-animation-timing-function: ease-in; - animation-timing-function: ease-in; - opacity: 0; - } - - 40% { - -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -20deg); - transform: perspective(400px) rotate3d(0, 1, 0, -20deg); - -webkit-animation-timing-function: ease-in; - animation-timing-function: ease-in; - } - - 60% { - -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 10deg); - transform: perspective(400px) rotate3d(0, 1, 0, 10deg); - opacity: 1; - } - - 80% { - -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -5deg); - transform: perspective(400px) rotate3d(0, 1, 0, -5deg); - } - - to { - -webkit-transform: perspective(400px); - transform: perspective(400px); - } -} - -.flipInY { - -webkit-backface-visibility: visible !important; - backface-visibility: visible !important; - -webkit-animation-name: flipInY; - animation-name: flipInY; -} - -@-webkit-keyframes flipOutX { - from { - -webkit-transform: perspective(400px); - transform: perspective(400px); - } - - 30% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg); - transform: perspective(400px) rotate3d(1, 0, 0, -20deg); - opacity: 1; - } - - to { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg); - transform: perspective(400px) rotate3d(1, 0, 0, 90deg); - opacity: 0; - } -} - -@keyframes flipOutX { - from { - -webkit-transform: perspective(400px); - transform: perspective(400px); - } - - 30% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg); - transform: perspective(400px) rotate3d(1, 0, 0, -20deg); - opacity: 1; - } - - to { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg); - transform: perspective(400px) rotate3d(1, 0, 0, 90deg); - opacity: 0; - } -} - -.flipOutX { - -webkit-animation-duration: 0.75s; - animation-duration: 0.75s; - -webkit-animation-name: flipOutX; - animation-name: flipOutX; - -webkit-backface-visibility: visible !important; - backface-visibility: visible !important; -} - -@-webkit-keyframes flipOutY { - from { - -webkit-transform: perspective(400px); - transform: perspective(400px); - } - - 30% { - -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -15deg); - transform: perspective(400px) rotate3d(0, 1, 0, -15deg); - opacity: 1; - } - - to { - -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg); - transform: perspective(400px) rotate3d(0, 1, 0, 90deg); - opacity: 0; - } -} - -@keyframes flipOutY { - from { - -webkit-transform: perspective(400px); - transform: perspective(400px); - } - - 30% { - -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -15deg); - transform: perspective(400px) rotate3d(0, 1, 0, -15deg); - opacity: 1; - } - - to { - -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg); - transform: perspective(400px) rotate3d(0, 1, 0, 90deg); - opacity: 0; - } -} - -.flipOutY { - -webkit-animation-duration: 0.75s; - animation-duration: 0.75s; - -webkit-backface-visibility: visible !important; - backface-visibility: visible !important; - -webkit-animation-name: flipOutY; - animation-name: flipOutY; -} - -@-webkit-keyframes lightSpeedIn { - from { - -webkit-transform: translate3d(100%, 0, 0) skewX(-30deg); - transform: translate3d(100%, 0, 0) skewX(-30deg); - opacity: 0; - } - - 60% { - -webkit-transform: skewX(20deg); - transform: skewX(20deg); - opacity: 1; - } - - 80% { - -webkit-transform: skewX(-5deg); - transform: skewX(-5deg); - } - - to { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -@keyframes lightSpeedIn { - from { - -webkit-transform: translate3d(100%, 0, 0) skewX(-30deg); - transform: translate3d(100%, 0, 0) skewX(-30deg); - opacity: 0; - } - - 60% { - -webkit-transform: skewX(20deg); - transform: skewX(20deg); - opacity: 1; - } - - 80% { - -webkit-transform: skewX(-5deg); - transform: skewX(-5deg); - } - - to { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -.lightSpeedIn { - -webkit-animation-name: lightSpeedIn; - animation-name: lightSpeedIn; - -webkit-animation-timing-function: ease-out; - animation-timing-function: ease-out; -} - -@-webkit-keyframes lightSpeedOut { - from { - opacity: 1; - } - - to { - -webkit-transform: translate3d(100%, 0, 0) skewX(30deg); - transform: translate3d(100%, 0, 0) skewX(30deg); - opacity: 0; - } -} - -@keyframes lightSpeedOut { - from { - opacity: 1; - } - - to { - -webkit-transform: translate3d(100%, 0, 0) skewX(30deg); - transform: translate3d(100%, 0, 0) skewX(30deg); - opacity: 0; - } -} - -.lightSpeedOut { - -webkit-animation-name: lightSpeedOut; - animation-name: lightSpeedOut; - -webkit-animation-timing-function: ease-in; - animation-timing-function: ease-in; -} - -@-webkit-keyframes rotateIn { - from { - -webkit-transform-origin: center; - transform-origin: center; - -webkit-transform: rotate3d(0, 0, 1, -200deg); - transform: rotate3d(0, 0, 1, -200deg); - opacity: 0; - } - - to { - -webkit-transform-origin: center; - transform-origin: center; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - opacity: 1; - } -} - -@keyframes rotateIn { - from { - -webkit-transform-origin: center; - transform-origin: center; - -webkit-transform: rotate3d(0, 0, 1, -200deg); - transform: rotate3d(0, 0, 1, -200deg); - opacity: 0; - } - - to { - -webkit-transform-origin: center; - transform-origin: center; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - opacity: 1; - } -} - -.rotateIn { - -webkit-animation-name: rotateIn; - animation-name: rotateIn; -} - -@-webkit-keyframes rotateInDownLeft { - from { - -webkit-transform-origin: left bottom; - transform-origin: left bottom; - -webkit-transform: rotate3d(0, 0, 1, -45deg); - transform: rotate3d(0, 0, 1, -45deg); - opacity: 0; - } - - to { - -webkit-transform-origin: left bottom; - transform-origin: left bottom; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - opacity: 1; - } -} - -@keyframes rotateInDownLeft { - from { - -webkit-transform-origin: left bottom; - transform-origin: left bottom; - -webkit-transform: rotate3d(0, 0, 1, -45deg); - transform: rotate3d(0, 0, 1, -45deg); - opacity: 0; - } - - to { - -webkit-transform-origin: left bottom; - transform-origin: left bottom; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - opacity: 1; - } -} - -.rotateInDownLeft { - -webkit-animation-name: rotateInDownLeft; - animation-name: rotateInDownLeft; -} - -@-webkit-keyframes rotateInDownRight { - from { - -webkit-transform-origin: right bottom; - transform-origin: right bottom; - -webkit-transform: rotate3d(0, 0, 1, 45deg); - transform: rotate3d(0, 0, 1, 45deg); - opacity: 0; - } - - to { - -webkit-transform-origin: right bottom; - transform-origin: right bottom; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - opacity: 1; - } -} - -@keyframes rotateInDownRight { - from { - -webkit-transform-origin: right bottom; - transform-origin: right bottom; - -webkit-transform: rotate3d(0, 0, 1, 45deg); - transform: rotate3d(0, 0, 1, 45deg); - opacity: 0; - } - - to { - -webkit-transform-origin: right bottom; - transform-origin: right bottom; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - opacity: 1; - } -} - -.rotateInDownRight { - -webkit-animation-name: rotateInDownRight; - animation-name: rotateInDownRight; -} - -@-webkit-keyframes rotateInUpLeft { - from { - -webkit-transform-origin: left bottom; - transform-origin: left bottom; - -webkit-transform: rotate3d(0, 0, 1, 45deg); - transform: rotate3d(0, 0, 1, 45deg); - opacity: 0; - } - - to { - -webkit-transform-origin: left bottom; - transform-origin: left bottom; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - opacity: 1; - } -} - -@keyframes rotateInUpLeft { - from { - -webkit-transform-origin: left bottom; - transform-origin: left bottom; - -webkit-transform: rotate3d(0, 0, 1, 45deg); - transform: rotate3d(0, 0, 1, 45deg); - opacity: 0; - } - - to { - -webkit-transform-origin: left bottom; - transform-origin: left bottom; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - opacity: 1; - } -} - -.rotateInUpLeft { - -webkit-animation-name: rotateInUpLeft; - animation-name: rotateInUpLeft; -} - -@-webkit-keyframes rotateInUpRight { - from { - -webkit-transform-origin: right bottom; - transform-origin: right bottom; - -webkit-transform: rotate3d(0, 0, 1, -90deg); - transform: rotate3d(0, 0, 1, -90deg); - opacity: 0; - } - - to { - -webkit-transform-origin: right bottom; - transform-origin: right bottom; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - opacity: 1; - } -} - -@keyframes rotateInUpRight { - from { - -webkit-transform-origin: right bottom; - transform-origin: right bottom; - -webkit-transform: rotate3d(0, 0, 1, -90deg); - transform: rotate3d(0, 0, 1, -90deg); - opacity: 0; - } - - to { - -webkit-transform-origin: right bottom; - transform-origin: right bottom; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - opacity: 1; - } -} - -.rotateInUpRight { - -webkit-animation-name: rotateInUpRight; - animation-name: rotateInUpRight; -} - -@-webkit-keyframes rotateOut { - from { - -webkit-transform-origin: center; - transform-origin: center; - opacity: 1; - } - - to { - -webkit-transform-origin: center; - transform-origin: center; - -webkit-transform: rotate3d(0, 0, 1, 200deg); - transform: rotate3d(0, 0, 1, 200deg); - opacity: 0; - } -} - -@keyframes rotateOut { - from { - -webkit-transform-origin: center; - transform-origin: center; - opacity: 1; - } - - to { - -webkit-transform-origin: center; - transform-origin: center; - -webkit-transform: rotate3d(0, 0, 1, 200deg); - transform: rotate3d(0, 0, 1, 200deg); - opacity: 0; - } -} - -.rotateOut { - -webkit-animation-name: rotateOut; - animation-name: rotateOut; -} - -@-webkit-keyframes rotateOutDownLeft { - from { - -webkit-transform-origin: left bottom; - transform-origin: left bottom; - opacity: 1; - } - - to { - -webkit-transform-origin: left bottom; - transform-origin: left bottom; - -webkit-transform: rotate3d(0, 0, 1, 45deg); - transform: rotate3d(0, 0, 1, 45deg); - opacity: 0; - } -} - -@keyframes rotateOutDownLeft { - from { - -webkit-transform-origin: left bottom; - transform-origin: left bottom; - opacity: 1; - } - - to { - -webkit-transform-origin: left bottom; - transform-origin: left bottom; - -webkit-transform: rotate3d(0, 0, 1, 45deg); - transform: rotate3d(0, 0, 1, 45deg); - opacity: 0; - } -} - -.rotateOutDownLeft { - -webkit-animation-name: rotateOutDownLeft; - animation-name: rotateOutDownLeft; -} - -@-webkit-keyframes rotateOutDownRight { - from { - -webkit-transform-origin: right bottom; - transform-origin: right bottom; - opacity: 1; - } - - to { - -webkit-transform-origin: right bottom; - transform-origin: right bottom; - -webkit-transform: rotate3d(0, 0, 1, -45deg); - transform: rotate3d(0, 0, 1, -45deg); - opacity: 0; - } -} - -@keyframes rotateOutDownRight { - from { - -webkit-transform-origin: right bottom; - transform-origin: right bottom; - opacity: 1; - } - - to { - -webkit-transform-origin: right bottom; - transform-origin: right bottom; - -webkit-transform: rotate3d(0, 0, 1, -45deg); - transform: rotate3d(0, 0, 1, -45deg); - opacity: 0; - } -} - -.rotateOutDownRight { - -webkit-animation-name: rotateOutDownRight; - animation-name: rotateOutDownRight; -} - -@-webkit-keyframes rotateOutUpLeft { - from { - -webkit-transform-origin: left bottom; - transform-origin: left bottom; - opacity: 1; - } - - to { - -webkit-transform-origin: left bottom; - transform-origin: left bottom; - -webkit-transform: rotate3d(0, 0, 1, -45deg); - transform: rotate3d(0, 0, 1, -45deg); - opacity: 0; - } -} - -@keyframes rotateOutUpLeft { - from { - -webkit-transform-origin: left bottom; - transform-origin: left bottom; - opacity: 1; - } - - to { - -webkit-transform-origin: left bottom; - transform-origin: left bottom; - -webkit-transform: rotate3d(0, 0, 1, -45deg); - transform: rotate3d(0, 0, 1, -45deg); - opacity: 0; - } -} - -.rotateOutUpLeft { - -webkit-animation-name: rotateOutUpLeft; - animation-name: rotateOutUpLeft; -} - -@-webkit-keyframes rotateOutUpRight { - from { - -webkit-transform-origin: right bottom; - transform-origin: right bottom; - opacity: 1; - } - - to { - -webkit-transform-origin: right bottom; - transform-origin: right bottom; - -webkit-transform: rotate3d(0, 0, 1, 90deg); - transform: rotate3d(0, 0, 1, 90deg); - opacity: 0; - } -} - -@keyframes rotateOutUpRight { - from { - -webkit-transform-origin: right bottom; - transform-origin: right bottom; - opacity: 1; - } - - to { - -webkit-transform-origin: right bottom; - transform-origin: right bottom; - -webkit-transform: rotate3d(0, 0, 1, 90deg); - transform: rotate3d(0, 0, 1, 90deg); - opacity: 0; - } -} - -.rotateOutUpRight { - -webkit-animation-name: rotateOutUpRight; - animation-name: rotateOutUpRight; -} - -@-webkit-keyframes hinge { - 0% { - -webkit-transform-origin: top left; - transform-origin: top left; - -webkit-animation-timing-function: ease-in-out; - animation-timing-function: ease-in-out; - } - - 20%, - 60% { - -webkit-transform: rotate3d(0, 0, 1, 80deg); - transform: rotate3d(0, 0, 1, 80deg); - -webkit-transform-origin: top left; - transform-origin: top left; - -webkit-animation-timing-function: ease-in-out; - animation-timing-function: ease-in-out; - } - - 40%, - 80% { - -webkit-transform: rotate3d(0, 0, 1, 60deg); - transform: rotate3d(0, 0, 1, 60deg); - -webkit-transform-origin: top left; - transform-origin: top left; - -webkit-animation-timing-function: ease-in-out; - animation-timing-function: ease-in-out; - opacity: 1; - } - - to { - -webkit-transform: translate3d(0, 700px, 0); - transform: translate3d(0, 700px, 0); - opacity: 0; - } -} - -@keyframes hinge { - 0% { - -webkit-transform-origin: top left; - transform-origin: top left; - -webkit-animation-timing-function: ease-in-out; - animation-timing-function: ease-in-out; - } - - 20%, - 60% { - -webkit-transform: rotate3d(0, 0, 1, 80deg); - transform: rotate3d(0, 0, 1, 80deg); - -webkit-transform-origin: top left; - transform-origin: top left; - -webkit-animation-timing-function: ease-in-out; - animation-timing-function: ease-in-out; - } - - 40%, - 80% { - -webkit-transform: rotate3d(0, 0, 1, 60deg); - transform: rotate3d(0, 0, 1, 60deg); - -webkit-transform-origin: top left; - transform-origin: top left; - -webkit-animation-timing-function: ease-in-out; - animation-timing-function: ease-in-out; - opacity: 1; - } - - to { - -webkit-transform: translate3d(0, 700px, 0); - transform: translate3d(0, 700px, 0); - opacity: 0; - } -} - -.hinge { - -webkit-animation-duration: 2s; - animation-duration: 2s; - -webkit-animation-name: hinge; - animation-name: hinge; -} - -@-webkit-keyframes jackInTheBox { - from { - opacity: 0; - -webkit-transform: scale(0.1) rotate(30deg); - transform: scale(0.1) rotate(30deg); - -webkit-transform-origin: center bottom; - transform-origin: center bottom; - } - - 50% { - -webkit-transform: rotate(-10deg); - transform: rotate(-10deg); - } - - 70% { - -webkit-transform: rotate(3deg); - transform: rotate(3deg); - } - - to { - opacity: 1; - -webkit-transform: scale(1); - transform: scale(1); - } -} - -@keyframes jackInTheBox { - from { - opacity: 0; - -webkit-transform: scale(0.1) rotate(30deg); - transform: scale(0.1) rotate(30deg); - -webkit-transform-origin: center bottom; - transform-origin: center bottom; - } - - 50% { - -webkit-transform: rotate(-10deg); - transform: rotate(-10deg); - } - - 70% { - -webkit-transform: rotate(3deg); - transform: rotate(3deg); - } - - to { - opacity: 1; - -webkit-transform: scale(1); - transform: scale(1); - } -} - -.jackInTheBox { - -webkit-animation-name: jackInTheBox; - animation-name: jackInTheBox; -} - -/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */ - -@-webkit-keyframes rollIn { - from { - opacity: 0; - -webkit-transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg); - transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg); - } - - to { - opacity: 1; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -@keyframes rollIn { - from { - opacity: 0; - -webkit-transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg); - transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg); - } - - to { - opacity: 1; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -.rollIn { - -webkit-animation-name: rollIn; - animation-name: rollIn; -} - -/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */ - -@-webkit-keyframes rollOut { - from { - opacity: 1; - } - - to { - opacity: 0; - -webkit-transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg); - transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg); - } -} - -@keyframes rollOut { - from { - opacity: 1; - } - - to { - opacity: 0; - -webkit-transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg); - transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg); - } -} - -.rollOut { - -webkit-animation-name: rollOut; - animation-name: rollOut; -} - -@-webkit-keyframes zoomIn { - from { - opacity: 0; - -webkit-transform: scale3d(0.3, 0.3, 0.3); - transform: scale3d(0.3, 0.3, 0.3); - } - - 50% { - opacity: 1; - } -} - -@keyframes zoomIn { - from { - opacity: 0; - -webkit-transform: scale3d(0.3, 0.3, 0.3); - transform: scale3d(0.3, 0.3, 0.3); - } - - 50% { - opacity: 1; - } -} - -.zoomIn { - -webkit-animation-name: zoomIn; - animation-name: zoomIn; -} - -@-webkit-keyframes zoomInDown { - from { - opacity: 0; - -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0); - transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0); - -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - } - - 60% { - opacity: 1; - -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); - transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); - -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - } -} - -@keyframes zoomInDown { - from { - opacity: 0; - -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0); - transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0); - -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - } - - 60% { - opacity: 1; - -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); - transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); - -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - } -} - -.zoomInDown { - -webkit-animation-name: zoomInDown; - animation-name: zoomInDown; -} - -@-webkit-keyframes zoomInLeft { - from { - opacity: 0; - -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(-1000px, 0, 0); - transform: scale3d(0.1, 0.1, 0.1) translate3d(-1000px, 0, 0); - -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - } - - 60% { - opacity: 1; - -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(10px, 0, 0); - transform: scale3d(0.475, 0.475, 0.475) translate3d(10px, 0, 0); - -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - } -} - -@keyframes zoomInLeft { - from { - opacity: 0; - -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(-1000px, 0, 0); - transform: scale3d(0.1, 0.1, 0.1) translate3d(-1000px, 0, 0); - -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - } - - 60% { - opacity: 1; - -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(10px, 0, 0); - transform: scale3d(0.475, 0.475, 0.475) translate3d(10px, 0, 0); - -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - } -} - -.zoomInLeft { - -webkit-animation-name: zoomInLeft; - animation-name: zoomInLeft; -} - -@-webkit-keyframes zoomInRight { - from { - opacity: 0; - -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(1000px, 0, 0); - transform: scale3d(0.1, 0.1, 0.1) translate3d(1000px, 0, 0); - -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - } - - 60% { - opacity: 1; - -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(-10px, 0, 0); - transform: scale3d(0.475, 0.475, 0.475) translate3d(-10px, 0, 0); - -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - } -} - -@keyframes zoomInRight { - from { - opacity: 0; - -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(1000px, 0, 0); - transform: scale3d(0.1, 0.1, 0.1) translate3d(1000px, 0, 0); - -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - } - - 60% { - opacity: 1; - -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(-10px, 0, 0); - transform: scale3d(0.475, 0.475, 0.475) translate3d(-10px, 0, 0); - -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - } -} - -.zoomInRight { - -webkit-animation-name: zoomInRight; - animation-name: zoomInRight; -} - -@-webkit-keyframes zoomInUp { - from { - opacity: 0; - -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 1000px, 0); - transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 1000px, 0); - -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - } - - 60% { - opacity: 1; - -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); - transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); - -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - } -} - -@keyframes zoomInUp { - from { - opacity: 0; - -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 1000px, 0); - transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 1000px, 0); - -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - } - - 60% { - opacity: 1; - -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); - transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); - -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - } -} - -.zoomInUp { - -webkit-animation-name: zoomInUp; - animation-name: zoomInUp; -} - -@-webkit-keyframes zoomOut { - from { - opacity: 1; - } - - 50% { - opacity: 0; - -webkit-transform: scale3d(0.3, 0.3, 0.3); - transform: scale3d(0.3, 0.3, 0.3); - } - - to { - opacity: 0; - } -} - -@keyframes zoomOut { - from { - opacity: 1; - } - - 50% { - opacity: 0; - -webkit-transform: scale3d(0.3, 0.3, 0.3); - transform: scale3d(0.3, 0.3, 0.3); - } - - to { - opacity: 0; - } -} - -.zoomOut { - -webkit-animation-name: zoomOut; - animation-name: zoomOut; -} - -@-webkit-keyframes zoomOutDown { - 40% { - opacity: 1; - -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); - transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); - -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - } - - to { - opacity: 0; - -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 2000px, 0); - transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 2000px, 0); - -webkit-transform-origin: center bottom; - transform-origin: center bottom; - -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - } -} - -@keyframes zoomOutDown { - 40% { - opacity: 1; - -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); - transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); - -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - } - - to { - opacity: 0; - -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 2000px, 0); - transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 2000px, 0); - -webkit-transform-origin: center bottom; - transform-origin: center bottom; - -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - } -} - -.zoomOutDown { - -webkit-animation-name: zoomOutDown; - animation-name: zoomOutDown; -} - -@-webkit-keyframes zoomOutLeft { - 40% { - opacity: 1; - -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(42px, 0, 0); - transform: scale3d(0.475, 0.475, 0.475) translate3d(42px, 0, 0); - } - - to { - opacity: 0; - -webkit-transform: scale(0.1) translate3d(-2000px, 0, 0); - transform: scale(0.1) translate3d(-2000px, 0, 0); - -webkit-transform-origin: left center; - transform-origin: left center; - } -} - -@keyframes zoomOutLeft { - 40% { - opacity: 1; - -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(42px, 0, 0); - transform: scale3d(0.475, 0.475, 0.475) translate3d(42px, 0, 0); - } - - to { - opacity: 0; - -webkit-transform: scale(0.1) translate3d(-2000px, 0, 0); - transform: scale(0.1) translate3d(-2000px, 0, 0); - -webkit-transform-origin: left center; - transform-origin: left center; - } -} - -.zoomOutLeft { - -webkit-animation-name: zoomOutLeft; - animation-name: zoomOutLeft; -} - -@-webkit-keyframes zoomOutRight { - 40% { - opacity: 1; - -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(-42px, 0, 0); - transform: scale3d(0.475, 0.475, 0.475) translate3d(-42px, 0, 0); - } - - to { - opacity: 0; - -webkit-transform: scale(0.1) translate3d(2000px, 0, 0); - transform: scale(0.1) translate3d(2000px, 0, 0); - -webkit-transform-origin: right center; - transform-origin: right center; - } -} - -@keyframes zoomOutRight { - 40% { - opacity: 1; - -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(-42px, 0, 0); - transform: scale3d(0.475, 0.475, 0.475) translate3d(-42px, 0, 0); - } - - to { - opacity: 0; - -webkit-transform: scale(0.1) translate3d(2000px, 0, 0); - transform: scale(0.1) translate3d(2000px, 0, 0); - -webkit-transform-origin: right center; - transform-origin: right center; - } -} - -.zoomOutRight { - -webkit-animation-name: zoomOutRight; - animation-name: zoomOutRight; -} - -@-webkit-keyframes zoomOutUp { - 40% { - opacity: 1; - -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); - transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); - -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - } - - to { - opacity: 0; - -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -2000px, 0); - transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -2000px, 0); - -webkit-transform-origin: center bottom; - transform-origin: center bottom; - -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - } -} - -@keyframes zoomOutUp { - 40% { - opacity: 1; - -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); - transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); - -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - } - - to { - opacity: 0; - -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -2000px, 0); - transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -2000px, 0); - -webkit-transform-origin: center bottom; - transform-origin: center bottom; - -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - } -} - -.zoomOutUp { - -webkit-animation-name: zoomOutUp; - animation-name: zoomOutUp; -} - -@-webkit-keyframes slideInDown { - from { - -webkit-transform: translate3d(0, -100%, 0); - transform: translate3d(0, -100%, 0); - visibility: visible; - } - - to { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -@keyframes slideInDown { - from { - -webkit-transform: translate3d(0, -100%, 0); - transform: translate3d(0, -100%, 0); - visibility: visible; - } - - to { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -.slideInDown { - -webkit-animation-name: slideInDown; - animation-name: slideInDown; -} - -@-webkit-keyframes slideInLeft { - from { - -webkit-transform: translate3d(-100%, 0, 0); - transform: translate3d(-100%, 0, 0); - visibility: visible; - } - - to { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -@keyframes slideInLeft { - from { - -webkit-transform: translate3d(-100%, 0, 0); - transform: translate3d(-100%, 0, 0); - visibility: visible; - } - - to { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -.slideInLeft { - -webkit-animation-name: slideInLeft; - animation-name: slideInLeft; -} - -@-webkit-keyframes slideInRight { - from { - -webkit-transform: translate3d(100%, 0, 0); - transform: translate3d(100%, 0, 0); - visibility: visible; - } - - to { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -@keyframes slideInRight { - from { - -webkit-transform: translate3d(100%, 0, 0); - transform: translate3d(100%, 0, 0); - visibility: visible; - } - - to { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -.slideInRight { - -webkit-animation-name: slideInRight; - animation-name: slideInRight; -} - -@-webkit-keyframes slideInUp { - from { - -webkit-transform: translate3d(0, 100%, 0); - transform: translate3d(0, 100%, 0); - visibility: visible; - } - - to { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -@keyframes slideInUp { - from { - -webkit-transform: translate3d(0, 100%, 0); - transform: translate3d(0, 100%, 0); - visibility: visible; - } - - to { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -.slideInUp { - -webkit-animation-name: slideInUp; - animation-name: slideInUp; -} - -@-webkit-keyframes slideOutDown { - from { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } - - to { - visibility: hidden; - -webkit-transform: translate3d(0, 100%, 0); - transform: translate3d(0, 100%, 0); - } -} - -@keyframes slideOutDown { - from { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } - - to { - visibility: hidden; - -webkit-transform: translate3d(0, 100%, 0); - transform: translate3d(0, 100%, 0); - } -} - -.slideOutDown { - -webkit-animation-name: slideOutDown; - animation-name: slideOutDown; -} - -@-webkit-keyframes slideOutLeft { - from { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } - - to { - visibility: hidden; - -webkit-transform: translate3d(-100%, 0, 0); - transform: translate3d(-100%, 0, 0); - } -} - -@keyframes slideOutLeft { - from { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } - - to { - visibility: hidden; - -webkit-transform: translate3d(-100%, 0, 0); - transform: translate3d(-100%, 0, 0); - } -} - -.slideOutLeft { - -webkit-animation-name: slideOutLeft; - animation-name: slideOutLeft; -} - -@-webkit-keyframes slideOutRight { - from { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } - - to { - visibility: hidden; - -webkit-transform: translate3d(100%, 0, 0); - transform: translate3d(100%, 0, 0); - } -} - -@keyframes slideOutRight { - from { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } - - to { - visibility: hidden; - -webkit-transform: translate3d(100%, 0, 0); - transform: translate3d(100%, 0, 0); - } -} - -.slideOutRight { - -webkit-animation-name: slideOutRight; - animation-name: slideOutRight; -} - -@-webkit-keyframes slideOutUp { - from { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } - - to { - visibility: hidden; - -webkit-transform: translate3d(0, -100%, 0); - transform: translate3d(0, -100%, 0); - } -} - -@keyframes slideOutUp { - from { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } - - to { - visibility: hidden; - -webkit-transform: translate3d(0, -100%, 0); - transform: translate3d(0, -100%, 0); - } -} - -.slideOutUp { - -webkit-animation-name: slideOutUp; - animation-name: slideOutUp; -} - -.animated { - -webkit-animation-duration: 0.4s; - animation-duration: 0.4s; - -webkit-animation-fill-mode: both; - animation-fill-mode: both; -} - -.animated.infinite { - -webkit-animation-iteration-count: infinite; - animation-iteration-count: infinite; -} - -.animated.delay-1s { - -webkit-animation-delay: 1s; - animation-delay: 1s; -} - -.animated.delay-2s { - -webkit-animation-delay: 2s; - animation-delay: 2s; -} - -.animated.delay-3s { - -webkit-animation-delay: 3s; - animation-delay: 3s; -} - -.animated.delay-4s { - -webkit-animation-delay: 4s; - animation-delay: 4s; -} - -.animated.delay-5s { - -webkit-animation-delay: 5s; - animation-delay: 5s; -} - -.animated.fast { - -webkit-animation-duration: 800ms; - animation-duration: 800ms; -} - -.animated.faster { - -webkit-animation-duration: 500ms; - animation-duration: 500ms; -} - -.animated.slow { - -webkit-animation-duration: 2s; - animation-duration: 2s; -} - -.animated.slower { - -webkit-animation-duration: 3s; - animation-duration: 3s; -} - -@media (print), (prefers-reduced-motion: reduce) { - .animated { - -webkit-animation-duration: 1ms !important; - animation-duration: 1ms !important; - -webkit-transition-duration: 1ms !important; - transition-duration: 1ms !important; - -webkit-animation-iteration-count: 1 !important; - animation-iteration-count: 1 !important; - } -} \ No newline at end of file diff --git a/public/css/lib/bootstrap.min.css b/public/css/lib/bootstrap.min.css deleted file mode 100644 index 067614c..0000000 --- a/public/css/lib/bootstrap.min.css +++ /dev/null @@ -1,6 +0,0 @@ -/*! - * Bootstrap v4.0.0 (https://getbootstrap.com) - * Copyright 2011-2018 The Bootstrap Authors - * Copyright 2011-2018 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - */:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#007bff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning:#ffc107;--danger:#dc3545;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}@-ms-viewport{width:device-width}article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent;-webkit-text-decoration-skip:objects}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg:not(:root){overflow:hidden}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-family:inherit;font-weight:500;line-height:1.2;color:inherit}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:80%;color:#6c757d}.blockquote-footer::before{content:"\2014 \00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#6c757d}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}code{font-size:87.5%;color:#e83e8c;word-break:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:#212529}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.container-fluid{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;min-height:1px;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-1{-webkit-box-flex:0;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-webkit-box-flex:0;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-webkit-box-flex:0;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-webkit-box-flex:0;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-webkit-box-flex:0;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-webkit-box-flex:0;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-webkit-box-flex:0;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-webkit-box-flex:0;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-sm-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-sm-1{-webkit-box-flex:0;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-webkit-box-flex:0;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-webkit-box-flex:0;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-webkit-box-flex:0;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-webkit-box-flex:0;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-webkit-box-flex:0;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-webkit-box-flex:0;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-webkit-box-flex:0;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-sm-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-sm-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-sm-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-sm-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-sm-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-sm-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-sm-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-sm-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-sm-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-sm-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-sm-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-sm-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-sm-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-sm-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-md-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-md-1{-webkit-box-flex:0;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-webkit-box-flex:0;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-webkit-box-flex:0;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-webkit-box-flex:0;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-webkit-box-flex:0;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-webkit-box-flex:0;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-webkit-box-flex:0;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-webkit-box-flex:0;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-md-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-md-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-md-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-md-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-md-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-md-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-md-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-md-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-md-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-md-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-md-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-md-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-md-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-md-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-lg-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-lg-1{-webkit-box-flex:0;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-webkit-box-flex:0;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-webkit-box-flex:0;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-webkit-box-flex:0;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-webkit-box-flex:0;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-webkit-box-flex:0;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-webkit-box-flex:0;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-webkit-box-flex:0;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-lg-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-lg-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-lg-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-lg-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-lg-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-lg-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-lg-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-lg-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-lg-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-lg-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-lg-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-lg-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-lg-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-lg-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-xl-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-xl-1{-webkit-box-flex:0;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-webkit-box-flex:0;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-webkit-box-flex:0;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-webkit-box-flex:0;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-webkit-box-flex:0;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-webkit-box-flex:0;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-webkit-box-flex:0;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-webkit-box-flex:0;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-xl-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-xl-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-xl-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-xl-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-xl-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-xl-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-xl-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-xl-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-xl-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-xl-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-xl-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-xl-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-xl-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-xl-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.table{width:100%;max-width:100%;margin-bottom:1rem;background-color:transparent}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #dee2e6}.table thead th{vertical-align:bottom;border-bottom:2px solid #dee2e6}.table tbody+tbody{border-top:2px solid #dee2e6}.table .table{background-color:#fff}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #dee2e6}.table-bordered td,.table-bordered th{border:1px solid #dee2e6}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{background-color:rgba(0,0,0,.075)}.table-primary,.table-primary>td,.table-primary>th{background-color:#b8daff}.table-hover .table-primary:hover{background-color:#9fcdff}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#9fcdff}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#d6d8db}.table-hover .table-secondary:hover{background-color:#c8cbcf}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#c8cbcf}.table-success,.table-success>td,.table-success>th{background-color:#c3e6cb}.table-hover .table-success:hover{background-color:#b1dfbb}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#b1dfbb}.table-info,.table-info>td,.table-info>th{background-color:#bee5eb}.table-hover .table-info:hover{background-color:#abdde5}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#abdde5}.table-warning,.table-warning>td,.table-warning>th{background-color:#ffeeba}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#ffe8a1}.table-danger,.table-danger>td,.table-danger>th{background-color:#f5c6cb}.table-hover .table-danger:hover{background-color:#f1b0b7}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f1b0b7}.table-light,.table-light>td,.table-light>th{background-color:#fdfdfe}.table-hover .table-light:hover{background-color:#ececf6}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#ececf6}.table-dark,.table-dark>td,.table-dark>th{background-color:#c6c8ca}.table-hover .table-dark:hover{background-color:#b9bbbe}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b9bbbe}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table .thead-dark th{color:#fff;background-color:#212529;border-color:#32383e}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#dee2e6}.table-dark{color:#fff;background-color:#212529}.table-dark td,.table-dark th,.table-dark thead th{border-color:#32383e}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,.05)}.table-dark.table-hover tbody tr:hover{background-color:rgba(255,255,255,.075)}@media (max-width:575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-sm>.table-bordered{border:0}}@media (max-width:767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-md>.table-bordered{border:0}}@media (max-width:991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-lg>.table-bordered{border:0}}@media (max-width:1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:focus{color:#495057;background-color:#fff;border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.form-control::-webkit-input-placeholder{color:#6c757d;opacity:1}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control:-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}select.form-control:not([size]):not([multiple]){height:calc(2.25rem + 2px)}select.form-control:focus::-ms-value{color:#495057;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding-top:.375rem;padding-bottom:.375rem;margin-bottom:0;line-height:1.5;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm,.input-group-lg>.form-control-plaintext.form-control,.input-group-lg>.input-group-append>.form-control-plaintext.btn,.input-group-lg>.input-group-append>.form-control-plaintext.input-group-text,.input-group-lg>.input-group-prepend>.form-control-plaintext.btn,.input-group-lg>.input-group-prepend>.form-control-plaintext.input-group-text,.input-group-sm>.form-control-plaintext.form-control,.input-group-sm>.input-group-append>.form-control-plaintext.btn,.input-group-sm>.input-group-append>.form-control-plaintext.input-group-text,.input-group-sm>.input-group-prepend>.form-control-plaintext.btn,.input-group-sm>.input-group-prepend>.form-control-plaintext.input-group-text{padding-right:0;padding-left:0}.form-control-sm,.input-group-sm>.form-control,.input-group-sm>.input-group-append>.btn,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-prepend>.input-group-text{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group-sm>.input-group-append>select.btn:not([size]):not([multiple]),.input-group-sm>.input-group-append>select.input-group-text:not([size]):not([multiple]),.input-group-sm>.input-group-prepend>select.btn:not([size]):not([multiple]),.input-group-sm>.input-group-prepend>select.input-group-text:not([size]):not([multiple]),.input-group-sm>select.form-control:not([size]):not([multiple]),select.form-control-sm:not([size]):not([multiple]){height:calc(1.8125rem + 2px)}.form-control-lg,.input-group-lg>.form-control,.input-group-lg>.input-group-append>.btn,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-prepend>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-lg>.input-group-append>select.btn:not([size]):not([multiple]),.input-group-lg>.input-group-append>select.input-group-text:not([size]):not([multiple]),.input-group-lg>.input-group-prepend>select.btn:not([size]):not([multiple]),.input-group-lg>.input-group-prepend>select.input-group-text:not([size]):not([multiple]),.input-group-lg>select.form-control:not([size]):not([multiple]),select.form-control-lg:not([size]):not([multiple]){height:calc(2.875rem + 2px)}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*=col-]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input:disabled~.form-check-label{color:#6c757d}.form-check-label{margin-bottom:0}.form-check-inline{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#28a745}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.5rem;margin-top:.1rem;font-size:.875rem;line-height:1;color:#fff;background-color:rgba(40,167,69,.8);border-radius:.2rem}.custom-select.is-valid,.form-control.is-valid,.was-validated .custom-select:valid,.was-validated .form-control:valid{border-color:#28a745}.custom-select.is-valid:focus,.form-control.is-valid:focus,.was-validated .custom-select:valid:focus,.was-validated .form-control:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-select.is-valid~.valid-feedback,.custom-select.is-valid~.valid-tooltip,.form-control.is-valid~.valid-feedback,.form-control.is-valid~.valid-tooltip,.was-validated .custom-select:valid~.valid-feedback,.was-validated .custom-select:valid~.valid-tooltip,.was-validated .form-control:valid~.valid-feedback,.was-validated .form-control:valid~.valid-tooltip{display:block}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#28a745}.form-check-input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.was-validated .form-check-input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid~.custom-control-label,.was-validated .custom-control-input:valid~.custom-control-label{color:#28a745}.custom-control-input.is-valid~.custom-control-label::before,.was-validated .custom-control-input:valid~.custom-control-label::before{background-color:#71dd8a}.custom-control-input.is-valid~.valid-feedback,.custom-control-input.is-valid~.valid-tooltip,.was-validated .custom-control-input:valid~.valid-feedback,.was-validated .custom-control-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid:checked~.custom-control-label::before,.was-validated .custom-control-input:valid:checked~.custom-control-label::before{background-color:#34ce57}.custom-control-input.is-valid:focus~.custom-control-label::before,.was-validated .custom-control-input:valid:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(40,167,69,.25)}.custom-file-input.is-valid~.custom-file-label,.was-validated .custom-file-input:valid~.custom-file-label{border-color:#28a745}.custom-file-input.is-valid~.custom-file-label::before,.was-validated .custom-file-input:valid~.custom-file-label::before{border-color:inherit}.custom-file-input.is-valid~.valid-feedback,.custom-file-input.is-valid~.valid-tooltip,.was-validated .custom-file-input:valid~.valid-feedback,.was-validated .custom-file-input:valid~.valid-tooltip{display:block}.custom-file-input.is-valid:focus~.custom-file-label,.was-validated .custom-file-input:valid:focus~.custom-file-label{box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.5rem;margin-top:.1rem;font-size:.875rem;line-height:1;color:#fff;background-color:rgba(220,53,69,.8);border-radius:.2rem}.custom-select.is-invalid,.form-control.is-invalid,.was-validated .custom-select:invalid,.was-validated .form-control:invalid{border-color:#dc3545}.custom-select.is-invalid:focus,.form-control.is-invalid:focus,.was-validated .custom-select:invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.custom-select.is-invalid~.invalid-feedback,.custom-select.is-invalid~.invalid-tooltip,.form-control.is-invalid~.invalid-feedback,.form-control.is-invalid~.invalid-tooltip,.was-validated .custom-select:invalid~.invalid-feedback,.was-validated .custom-select:invalid~.invalid-tooltip,.was-validated .form-control:invalid~.invalid-feedback,.was-validated .form-control:invalid~.invalid-tooltip{display:block}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid~.custom-control-label,.was-validated .custom-control-input:invalid~.custom-control-label{color:#dc3545}.custom-control-input.is-invalid~.custom-control-label::before,.was-validated .custom-control-input:invalid~.custom-control-label::before{background-color:#efa2a9}.custom-control-input.is-invalid~.invalid-feedback,.custom-control-input.is-invalid~.invalid-tooltip,.was-validated .custom-control-input:invalid~.invalid-feedback,.was-validated .custom-control-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid:checked~.custom-control-label::before,.was-validated .custom-control-input:invalid:checked~.custom-control-label::before{background-color:#e4606d}.custom-control-input.is-invalid:focus~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(220,53,69,.25)}.custom-file-input.is-invalid~.custom-file-label,.was-validated .custom-file-input:invalid~.custom-file-label{border-color:#dc3545}.custom-file-input.is-invalid~.custom-file-label::before,.was-validated .custom-file-input:invalid~.custom-file-label::before{border-color:inherit}.custom-file-input.is-invalid~.invalid-feedback,.custom-file-input.is-invalid~.invalid-tooltip,.was-validated .custom-file-input:invalid~.invalid-feedback,.was-validated .custom-file-input:invalid~.invalid-tooltip{display:block}.custom-file-input.is-invalid:focus~.custom-file-label,.was-validated .custom-file-input:invalid:focus~.custom-file-label{box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-inline{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .input-group{width:auto}.form-inline .form-check{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;text-align:center;white-space:nowrap;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}.btn:focus,.btn:hover{text-decoration:none}.btn.focus,.btn:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.btn.disabled,.btn:disabled{opacity:.65}.btn:not(:disabled):not(.disabled){cursor:pointer}.btn:not(:disabled):not(.disabled).active,.btn:not(:disabled):not(.disabled):active{background-image:none}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:hover{color:#fff;background-color:#0069d9;border-color:#0062cc}.btn-primary.focus,.btn-primary:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0062cc;border-color:#005cbf}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5a6268;border-color:#545b62}.btn-secondary.focus,.btn-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#545b62;border-color:#4e555b}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:hover{color:#fff;background-color:#218838;border-color:#1e7e34}.btn-success.focus,.btn-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#1e7e34;border-color:#1c7430}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-info{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:hover{color:#fff;background-color:#138496;border-color:#117a8b}.btn-info.focus,.btn-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#117a8b;border-color:#10707f}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-warning{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#212529;background-color:#e0a800;border-color:#d39e00}.btn-warning.focus,.btn-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{color:#212529;background-color:#d39e00;border-color:#c69500}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#c82333;border-color:#bd2130}.btn-danger.focus,.btn-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#bd2130;border-color:#b21f2d}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-light{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#212529;background-color:#e2e6ea;border-color:#dae0e5}.btn-light.focus,.btn-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-light.disabled,.btn-light:disabled{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{color:#212529;background-color:#dae0e5;border-color:#d3d9df}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark.focus,.btn-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1d2124;border-color:#171a1d}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-primary{color:#007bff;background-color:transparent;background-image:none;border-color:#007bff}.btn-outline-primary:hover{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#007bff;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-secondary{color:#6c757d;background-color:transparent;background-image:none;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-success{color:#28a745;background-color:transparent;background-image:none;border-color:#28a745}.btn-outline-success:hover{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#28a745;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-info{color:#17a2b8;background-color:transparent;background-image:none;border-color:#17a2b8}.btn-outline-info:hover{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#17a2b8;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-warning{color:#ffc107;background-color:transparent;background-image:none;border-color:#ffc107}.btn-outline-warning:hover{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-danger{color:#dc3545;background-color:transparent;background-image:none;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-light{color:#f8f9fa;background-color:transparent;background-image:none;border-color:#f8f9fa}.btn-outline-light:hover{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-dark{color:#343a40;background-color:transparent;background-image:none;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-link{font-weight:400;color:#007bff;background-color:transparent}.btn-link:hover{color:#0056b3;text-decoration:underline;background-color:transparent;border-color:transparent}.btn-link.focus,.btn-link:focus{text-decoration:underline;border-color:transparent;box-shadow:none}.btn-link.disabled,.btn-link:disabled{color:#6c757d}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;transition:opacity .15s linear}.fade.show{opacity:1}.collapse{display:none}.collapse.show{display:block}tr.collapse.show{display:table-row}tbody.collapse.show{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;transition:height .35s ease}.dropdown,.dropup{position:relative}.dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropup .dropdown-menu{margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.255em;vertical-align:.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;width:0;height:0;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-item{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#16181b;text-decoration:none;background-color:#f8f9fa}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#007bff}.dropdown-item.disabled,.dropdown-item:disabled{color:#6c757d;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.btn-group,.btn-group-vertical{position:relative;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-webkit-box-flex:0;-ms-flex:0 1 auto;flex:0 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:1}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:1}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group,.btn-group-vertical .btn+.btn,.btn-group-vertical .btn+.btn-group,.btn-group-vertical .btn-group+.btn,.btn-group-vertical .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after{margin-left:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.btn-group-vertical .btn,.btn-group-vertical .btn-group{width:100%}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type=checkbox],.btn-group-toggle>.btn input[type=radio],.btn-group-toggle>.btn-group>.btn input[type=checkbox],.btn-group-toggle>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.custom-file,.input-group>.custom-select,.input-group>.form-control{position:relative;-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;margin-bottom:0}.input-group>.custom-file:focus,.input-group>.custom-select:focus,.input-group>.form-control:focus{z-index:3}.input-group>.custom-file+.custom-file,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.form-control,.input-group>.custom-select+.custom-file,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.form-control,.input-group>.form-control+.custom-file,.input-group>.form-control+.custom-select,.input-group>.form-control+.form-control{margin-left:-1px}.input-group>.custom-select:not(:last-child),.input-group>.form-control:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-select:not(:first-child),.input-group>.form-control:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::before{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label,.input-group>.custom-file:not(:first-child) .custom-file-label::before{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-append,.input-group-prepend{display:-webkit-box;display:-ms-flexbox;display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.btn,.input-group-append .input-group-text+.input-group-text,.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-prepend .input-group-text+.input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-text input[type=checkbox],.input-group-text input[type=radio]{margin-top:0}.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child),.input-group>.input-group-append:not(:last-child)>.btn,.input-group>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child),.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text{border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;display:block;min-height:1.5rem;padding-left:1.5rem}.custom-control-inline{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;z-index:-1;opacity:0}.custom-control-input:checked~.custom-control-label::before{color:#fff;background-color:#007bff}.custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-control-input:active~.custom-control-label::before{color:#fff;background-color:#b3d7ff}.custom-control-input:disabled~.custom-control-label{color:#6c757d}.custom-control-input:disabled~.custom-control-label::before{background-color:#e9ecef}.custom-control-label{margin-bottom:0}.custom-control-label::before{position:absolute;top:.25rem;left:0;display:block;width:1rem;height:1rem;pointer-events:none;content:"";-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:#dee2e6}.custom-control-label::after{position:absolute;top:.25rem;left:0;display:block;width:1rem;height:1rem;content:"";background-repeat:no-repeat;background-position:center center;background-size:50% 50%}.custom-checkbox .custom-control-label::before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-label::before{background-color:#007bff}.custom-checkbox .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::before{background-color:#007bff}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3E%3Cpath stroke='%23fff' d='M0 2h4'/%3E%3C/svg%3E")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label::before{background-color:#007bff}.custom-radio .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23fff'/%3E%3C/svg%3E")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-select{display:inline-block;width:100%;height:calc(2.25rem + 2px);padding:.375rem 1.75rem .375rem .75rem;line-height:1.5;color:#495057;vertical-align:middle;background:#fff url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E") no-repeat right .75rem center;background-size:8px 10px;border:1px solid #ced4da;border-radius:.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#80bdff;outline:0;box-shadow:inset 0 1px 2px rgba(0,0,0,.075),0 0 5px rgba(128,189,255,.5)}.custom-select:focus::-ms-value{color:#495057;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#6c757d;background-color:#e9ecef}.custom-select::-ms-expand{opacity:0}.custom-select-sm{height:calc(1.8125rem + 2px);padding-top:.375rem;padding-bottom:.375rem;font-size:75%}.custom-select-lg{height:calc(2.875rem + 2px);padding-top:.375rem;padding-bottom:.375rem;font-size:125%}.custom-file{position:relative;display:inline-block;width:100%;height:calc(2.25rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(2.25rem + 2px);margin:0;opacity:0}.custom-file-input:focus~.custom-file-control{border-color:#80bdff;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-file-input:focus~.custom-file-control::before{border-color:#80bdff}.custom-file-input:lang(en)~.custom-file-label::after{content:"Browse"}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(2.25rem + 2px);padding:.375rem .75rem;line-height:1.5;color:#495057;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:calc(calc(2.25rem + 2px) - 1px * 2);padding:.375rem .75rem;line-height:1.5;color:#495057;content:"Browse";background-color:#e9ecef;border-left:1px solid #ced4da;border-radius:0 .25rem .25rem 0}.nav{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#6c757d}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#007bff}.nav-fill .nav-item{-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;padding:.5rem 1rem}.navbar>.container,.navbar>.container-fluid{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler:not(:disabled):not(.disabled){cursor:pointer}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;background-size:100% 100%}@media (max-width:575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .dropdown-menu-right{right:0;left:auto}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .dropup .dropdown-menu{top:auto;bottom:100%}}@media (max-width:767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-expand-md{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .dropdown-menu-right{right:0;left:auto}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .dropup .dropdown-menu{top:auto;bottom:100%}}@media (max-width:991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-expand-lg{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .dropdown-menu-right{right:0;left:auto}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .dropup .dropdown-menu{top:auto;bottom:100%}}@media (max-width:1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-expand-xl{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .dropdown-menu-right{right:0;left:auto}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .dropup .dropdown-menu{top:auto;bottom:100%}}.navbar-expand{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .dropdown-menu-right{right:0;left:auto}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .dropup .dropdown-menu{top:auto;bottom:100%}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.5);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-light .navbar-text a{color:rgba(0,0,0,.9)}.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.5);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")}.navbar-dark .navbar-text{color:rgba(255,255,255,.5)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group:first-child .list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.card>.list-group:last-child .list-group-item:last-child{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-body{-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-header+.list-group .list-group-item:first-child{border-top:0}.card-footer{padding:.75rem 1.25rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img{width:100%;border-radius:calc(.25rem - 1px)}.card-img-top{width:100%;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img-bottom{width:100%;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-deck{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.card-deck .card{margin-bottom:15px}@media (min-width:576px){.card-deck{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-ms-flex:1 0 0%;flex:1 0 0%;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.card-group>.card{margin-bottom:15px}@media (min-width:576px){.card-group{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-webkit-box-flex:1;-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:first-child .card-header,.card-group>.card:first-child .card-img-top{border-top-right-radius:0}.card-group>.card:first-child .card-footer,.card-group>.card:first-child .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:last-child{border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:last-child .card-header,.card-group>.card:last-child .card-img-top{border-top-left-radius:0}.card-group>.card:last-child .card-footer,.card-group>.card:last-child .card-img-bottom{border-bottom-left-radius:0}.card-group>.card:only-child{border-radius:.25rem}.card-group>.card:only-child .card-header,.card-group>.card:only-child .card-img-top{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.card-group>.card:only-child .card-footer,.card-group>.card:only-child .card-img-bottom{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-group>.card:not(:first-child):not(:last-child):not(:only-child){border-radius:0}.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-footer,.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-header,.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-img-bottom,.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-img-top{border-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:576px){.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem}.card-columns .card{display:inline-block;width:100%}}.breadcrumb{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#e9ecef;border-radius:.25rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;padding-left:.5rem;color:#6c757d;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#6c757d}.pagination{display:-webkit-box;display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#007bff;background-color:#fff;border:1px solid #dee2e6}.page-link:hover{color:#0056b3;text-decoration:none;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:2;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.page-link:not(:disabled):not(.disabled){cursor:pointer}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:1;color:#fff;background-color:#007bff;border-color:#007bff}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dee2e6}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#007bff}.badge-primary[href]:focus,.badge-primary[href]:hover{color:#fff;text-decoration:none;background-color:#0062cc}.badge-secondary{color:#fff;background-color:#6c757d}.badge-secondary[href]:focus,.badge-secondary[href]:hover{color:#fff;text-decoration:none;background-color:#545b62}.badge-success{color:#fff;background-color:#28a745}.badge-success[href]:focus,.badge-success[href]:hover{color:#fff;text-decoration:none;background-color:#1e7e34}.badge-info{color:#fff;background-color:#17a2b8}.badge-info[href]:focus,.badge-info[href]:hover{color:#fff;text-decoration:none;background-color:#117a8b}.badge-warning{color:#212529;background-color:#ffc107}.badge-warning[href]:focus,.badge-warning[href]:hover{color:#212529;text-decoration:none;background-color:#d39e00}.badge-danger{color:#fff;background-color:#dc3545}.badge-danger[href]:focus,.badge-danger[href]:hover{color:#fff;text-decoration:none;background-color:#bd2130}.badge-light{color:#212529;background-color:#f8f9fa}.badge-light[href]:focus,.badge-light[href]:hover{color:#212529;text-decoration:none;background-color:#dae0e5}.badge-dark{color:#fff;background-color:#343a40}.badge-dark[href]:focus,.badge-dark[href]:hover{color:#fff;text-decoration:none;background-color:#1d2124}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff}.alert-primary hr{border-top-color:#9fcdff}.alert-primary .alert-link{color:#002752}.alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db}.alert-secondary hr{border-top-color:#c8cbcf}.alert-secondary .alert-link{color:#202326}.alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.alert-success hr{border-top-color:#b1dfbb}.alert-success .alert-link{color:#0b2e13}.alert-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb}.alert-info hr{border-top-color:#abdde5}.alert-info .alert-link{color:#062c33}.alert-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba}.alert-warning hr{border-top-color:#ffe8a1}.alert-warning .alert-link{color:#533f03}.alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.alert-danger hr{border-top-color:#f1b0b7}.alert-danger .alert-link{color:#491217}.alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe}.alert-light hr{border-top-color:#ececf6}.alert-light .alert-link{color:#686868}.alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.alert-dark hr{border-top-color:#b9bbbe}.alert-dark .alert-link{color:#040505}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-webkit-box;display:-ms-flexbox;display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;color:#fff;text-align:center;background-color:#007bff;transition:width .6s ease}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}.media{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start}.media-body{-webkit-box-flex:1;-ms-flex:1;flex:1}.list-group{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;margin-bottom:-1px;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.list-group-item:focus,.list-group-item:hover{z-index:1;text-decoration:none}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#007bff;border-color:#007bff}.list-group-flush .list-group-item{border-right:0;border-left:0;border-radius:0}.list-group-flush:first-child .list-group-item:first-child{border-top:0}.list-group-flush:last-child .list-group-item:last-child{border-bottom:0}.list-group-item-primary{color:#004085;background-color:#b8daff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#004085;background-color:#9fcdff}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#004085;border-color:#004085}.list-group-item-secondary{color:#383d41;background-color:#d6d8db}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#383d41;background-color:#c8cbcf}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#383d41;border-color:#383d41}.list-group-item-success{color:#155724;background-color:#c3e6cb}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#155724;background-color:#b1dfbb}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#155724;border-color:#155724}.list-group-item-info{color:#0c5460;background-color:#bee5eb}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#0c5460;background-color:#abdde5}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#0c5460;border-color:#0c5460}.list-group-item-warning{color:#856404;background-color:#ffeeba}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#856404;background-color:#ffe8a1}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#856404;border-color:#856404}.list-group-item-danger{color:#721c24;background-color:#f5c6cb}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#721c24;background-color:#f1b0b7}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#721c24;border-color:#721c24}.list-group-item-light{color:#818182;background-color:#fdfdfe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#818182;background-color:#ececf6}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#818182;border-color:#818182}.list-group-item-dark{color:#1b1e21;background-color:#c6c8ca}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#1b1e21;background-color:#b9bbbe}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#1b1e21;border-color:#1b1e21}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:focus,.close:hover{color:#000;text-decoration:none;opacity:.75}.close:not(:disabled):not(.disabled){cursor:pointer}button.close{padding:0;background-color:transparent;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;outline:0}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out;-webkit-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.show .modal-dialog{-webkit-transform:translate(0,0);transform:translate(0,0)}.modal-dialog-centered{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;min-height:calc(100% - (.5rem * 2))}.modal-content{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;padding:1rem;border-bottom:1px solid #e9ecef;border-top-left-radius:.3rem;border-top-right-radius:.3rem}.modal-header .close{padding:1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end;padding:1rem;border-top:1px solid #e9ecef}.modal-footer>:not(:first-child){margin-left:.25rem}.modal-footer>:not(:last-child){margin-right:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-centered{min-height:calc(100% - (1.75rem * 2))}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg{max-width:800px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^=top] .arrow::before,.bs-tooltip-top .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{padding:0 .4rem}.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=right] .arrow::before,.bs-tooltip-right .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^=bottom] .arrow::before,.bs-tooltip-bottom .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{padding:0 .4rem}.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=left] .arrow::before,.bs-tooltip-left .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow::after,.popover .arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[x-placement^=top],.bs-popover-top{margin-bottom:.5rem}.bs-popover-auto[x-placement^=top] .arrow,.bs-popover-top .arrow{bottom:calc((.5rem + 1px) * -1)}.bs-popover-auto[x-placement^=top] .arrow::after,.bs-popover-auto[x-placement^=top] .arrow::before,.bs-popover-top .arrow::after,.bs-popover-top .arrow::before{border-width:.5rem .5rem 0}.bs-popover-auto[x-placement^=top] .arrow::before,.bs-popover-top .arrow::before{bottom:0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=top] .arrow::after,.bs-popover-top .arrow::after{bottom:1px;border-top-color:#fff}.bs-popover-auto[x-placement^=right],.bs-popover-right{margin-left:.5rem}.bs-popover-auto[x-placement^=right] .arrow,.bs-popover-right .arrow{left:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=right] .arrow::after,.bs-popover-auto[x-placement^=right] .arrow::before,.bs-popover-right .arrow::after,.bs-popover-right .arrow::before{border-width:.5rem .5rem .5rem 0}.bs-popover-auto[x-placement^=right] .arrow::before,.bs-popover-right .arrow::before{left:0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=right] .arrow::after,.bs-popover-right .arrow::after{left:1px;border-right-color:#fff}.bs-popover-auto[x-placement^=bottom],.bs-popover-bottom{margin-top:.5rem}.bs-popover-auto[x-placement^=bottom] .arrow,.bs-popover-bottom .arrow{top:calc((.5rem + 1px) * -1)}.bs-popover-auto[x-placement^=bottom] .arrow::after,.bs-popover-auto[x-placement^=bottom] .arrow::before,.bs-popover-bottom .arrow::after,.bs-popover-bottom .arrow::before{border-width:0 .5rem .5rem .5rem}.bs-popover-auto[x-placement^=bottom] .arrow::before,.bs-popover-bottom .arrow::before{top:0;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=bottom] .arrow::after,.bs-popover-bottom .arrow::after{top:1px;border-bottom-color:#fff}.bs-popover-auto[x-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-auto[x-placement^=left],.bs-popover-left{margin-right:.5rem}.bs-popover-auto[x-placement^=left] .arrow,.bs-popover-left .arrow{right:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=left] .arrow::after,.bs-popover-auto[x-placement^=left] .arrow::before,.bs-popover-left .arrow::after,.bs-popover-left .arrow::before{border-width:.5rem 0 .5rem .5rem}.bs-popover-auto[x-placement^=left] .arrow::before,.bs-popover-left .arrow::before{right:0;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=left] .arrow::after,.bs-popover-left .arrow::after{right:1px;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;color:inherit;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#212529}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-item{position:relative;display:none;-webkit-box-align:center;-ms-flex-align:center;align-items:center;width:100%;transition:-webkit-transform .6s ease;transition:transform .6s ease;transition:transform .6s ease,-webkit-transform .6s ease;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.carousel-item-next,.carousel-item-prev{position:absolute;top:0}.carousel-item-next.carousel-item-left,.carousel-item-prev.carousel-item-right{-webkit-transform:translateX(0);transform:translateX(0)}@supports ((-webkit-transform-style:preserve-3d) or (transform-style:preserve-3d)){.carousel-item-next.carousel-item-left,.carousel-item-prev.carousel-item-right{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.active.carousel-item-right,.carousel-item-next{-webkit-transform:translateX(100%);transform:translateX(100%)}@supports ((-webkit-transform-style:preserve-3d) or (transform-style:preserve-3d)){.active.carousel-item-right,.carousel-item-next{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.active.carousel-item-left,.carousel-item-prev{-webkit-transform:translateX(-100%);transform:translateX(-100%)}@supports ((-webkit-transform-style:preserve-3d) or (transform-style:preserve-3d)){.active.carousel-item-left,.carousel-item-prev{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:transparent no-repeat center center;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M5.25 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3E%3C/svg%3E")}.carousel-control-next-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M2.75 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3E%3C/svg%3E")}.carousel-indicators{position:absolute;right:0;bottom:10px;left:0;z-index:15;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{position:relative;-webkit-box-flex:0;-ms-flex:0 1 auto;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;background-color:rgba(255,255,255,.5)}.carousel-indicators li::before{position:absolute;top:-10px;left:0;display:inline-block;width:100%;height:10px;content:""}.carousel-indicators li::after{position:absolute;bottom:-10px;left:0;display:inline-block;width:100%;height:10px;content:""}.carousel-indicators .active{background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-primary{background-color:#007bff!important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#0062cc!important}.bg-secondary{background-color:#6c757d!important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#545b62!important}.bg-success{background-color:#28a745!important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#1e7e34!important}.bg-info{background-color:#17a2b8!important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#117a8b!important}.bg-warning{background-color:#ffc107!important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#d39e00!important}.bg-danger{background-color:#dc3545!important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#bd2130!important}.bg-light{background-color:#f8f9fa!important}a.bg-light:focus,a.bg-light:hover,button.bg-light:focus,button.bg-light:hover{background-color:#dae0e5!important}.bg-dark{background-color:#343a40!important}a.bg-dark:focus,a.bg-dark:hover,button.bg-dark:focus,button.bg-dark:hover{background-color:#1d2124!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #dee2e6!important}.border-top{border-top:1px solid #dee2e6!important}.border-right{border-right:1px solid #dee2e6!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-left{border-left:1px solid #dee2e6!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#007bff!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#28a745!important}.border-info{border-color:#17a2b8!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#343a40!important}.border-white{border-color:#fff!important}.rounded{border-radius:.25rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-right{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-left{border-top-left-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-circle{border-radius:50%!important}.rounded-0{border-radius:0!important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-webkit-inline-box!important;display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-webkit-inline-box!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-webkit-inline-box!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-webkit-inline-box!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-webkit-inline-box!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important}.d-print-inline-flex{display:-webkit-inline-box!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.857143%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-start{-webkit-box-pack:start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-webkit-box-pack:end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-webkit-box-pack:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-webkit-box-pack:justify!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-webkit-box-align:start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-webkit-box-align:end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-webkit-box-align:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-webkit-box-align:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-webkit-box-align:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-sm-start{-webkit-box-pack:start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-webkit-box-pack:end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-webkit-box-pack:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-webkit-box-pack:justify!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-webkit-box-align:start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-webkit-box-align:end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-webkit-box-align:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-webkit-box-align:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-webkit-box-align:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-md-start{-webkit-box-pack:start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-webkit-box-pack:end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-webkit-box-pack:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-webkit-box-pack:justify!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-webkit-box-align:start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-webkit-box-align:end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-webkit-box-align:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-webkit-box-align:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-webkit-box-align:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-lg-start{-webkit-box-pack:start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-webkit-box-pack:end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-webkit-box-pack:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-webkit-box-pack:justify!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-webkit-box-align:start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-webkit-box-align:end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-webkit-box-align:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-webkit-box-align:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-webkit-box-align:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-xl-start{-webkit-box-pack:start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-webkit-box-pack:end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-webkit-box-pack:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-webkit-box-pack:justify!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-webkit-box-align:start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-webkit-box-align:end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-webkit-box-align:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-webkit-box-align:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-webkit-box-align:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports ((position:-webkit-sticky) or (position:sticky)){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;-webkit-clip-path:inset(50%);clip-path:inset(50%);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal;-webkit-clip-path:none;clip-path:none}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}.text-justify{text-align:justify!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-light{font-weight:300!important}.font-weight-normal{font-weight:400!important}.font-weight-bold{font-weight:700!important}.font-italic{font-style:italic!important}.text-white{color:#fff!important}.text-primary{color:#007bff!important}a.text-primary:focus,a.text-primary:hover{color:#0062cc!important}.text-secondary{color:#6c757d!important}a.text-secondary:focus,a.text-secondary:hover{color:#545b62!important}.text-success{color:#28a745!important}a.text-success:focus,a.text-success:hover{color:#1e7e34!important}.text-info{color:#17a2b8!important}a.text-info:focus,a.text-info:hover{color:#117a8b!important}.text-warning{color:#ffc107!important}a.text-warning:focus,a.text-warning:hover{color:#d39e00!important}.text-danger{color:#dc3545!important}a.text-danger:focus,a.text-danger:hover{color:#bd2130!important}.text-light{color:#f8f9fa!important}a.text-light:focus,a.text-light:hover{color:#dae0e5!important}.text-dark{color:#343a40!important}a.text-dark:focus,a.text-dark:hover{color:#1d2124!important}.text-muted{color:#6c757d!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media print{*,::after,::before{text-shadow:none!important;box-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px!important}.container{min-width:992px!important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}} \ No newline at end of file diff --git a/public/css/style.styl b/public/css/style.styl deleted file mode 100644 index 49236b8..0000000 --- a/public/css/style.styl +++ /dev/null @@ -1,1245 +0,0 @@ -@import url('https://rsms.me/inter/inter.css'); - -html { - height: 100%; -} - -body * { - font-family: 'Inter', sans-serif; -} - -raiseText() { - color: grey; - text-shadow: -1px 1px 0 white; - -moz-text-shadow: -1px 1px 0 white; - -webkit-text-shadow: -1px 1px 0 white; -} - -body { - margin: 0; - padding: 0; - height: 100% !important; - display: flex; -} - -h2 { - margin: 0 0 6px 0; - font-weight: bold; -} - -h6 { - margin: 2px 0 20px 0; -} - -hr { - margin: 14px 0; - border-top: 1px solid rgb(238, 238, 238); - border-bottom: 1px solid rgb(255, 255, 255); -} - -ul { - margin: 4px 0 0 0; -} - -label, h6 { - color: #000; - font-size: 13px; - font-weight: normal; -} - -.form-login { - margin-bottom: 0; - - .tab-title { - float: left; - margin-left: 10px; - } - - .block-header { - margin-top: 40px; - width: 580px; - margin: 40px auto 0; - - @media (max-width: 768px) { - width: 510px; - } - - @media (max-width: 576px) { - margin: 30px auto 0; - padding-right: 20px; - padding-left: 20px; - width: 100%; - } - } - - .button-top-right { - float: right; - margin: 0 10px 20px 0; - } - - .list-tabs { - width: 100%; - border-bottom: 2px solid rgba(0, 0, 0, 0.2); - - @media (min-width: 576px) { - width: 510px; - margin: auto; - } - - @media (min-width: 768px) { - width: 580px; - } - - .nav-item { - width: 33.3%; - margin-bottom: -2px; - - .nav-link { - border: none; - border-radius: none; - padding-left: 20px; - padding-right: 20px; - border-bottom: 2px solid rgba(0, 0, 0, 0.1); - color: #000; - opacity: 0.4; - font-weight: bold; - transition: all 0.2s; - - &.active { - background-color: unset !important; - border-bottom: 2px solid #000; - opacity: 1; - } - } - } - } - - .auth-server-selector { - select { - width: 100%; - float: right; - } - } - - .block-pnv { - display: none; - - &.active { - display: block; - animation: fadeIn 0.2s ease-out; - -webkit-animation: fadeIn 0.2s ease-out; - -moz-animation: fadeIn 0.2s ease-out; - } - } - - .block-login { - display: none; - - &.active { - display: block; - animation: fadeIn 0.2s ease-out; - -webkit-animation: fadeIn 0.2s ease-out; - -moz-animation: fadeIn 0.2s ease-out; - } - } - - .block-identity { - display: none; - - &.active { - display: block; - animation: fadeIn 0.2s ease-out; - -webkit-animation: fadeIn 0.2s ease-out; - -moz-animation: fadeIn 0.2s ease-out; - } - } -} - -.navbar-btn { - min-width: 100px; - margin-right: 8px; - - .btn { - min-width: 100px; - } -} - -.glyphicon { - margin-right: 8px; -} - -/* - login window -*/ -#login { - label { - margin-left: 2px; - } - - .app-name { - font-weight: bold; - margin-bottom: 2px; - font-size: 30px; - width: 30%; - line-height: 26px; - - @media (max-width: 576px) { - font-size: 24px; - line-height: 20px; - } - } - - .app-des { - font-size: 18px; - line-height: 1; - margin: 0; - - @media (max-width: 576px) { - font-size: 16px; - } - } - - .form-login { - padding: 0px; - margin: auto; - position: relative; - } - - .block-button { - position: relative; - - button { - background-color: unset; - border: 0; - cursor: pointer; - padding: 0px; - width: 100%; - text-align: left; - font-size: 16px; - font-weight: bold; - position: relative; - - &:focus { - outline: none; - -webkit-box-shadow: none; - box-shadow: none; - } - - .right-icon { - width: 24px; - height: 24px; - background-repeat: no-repeat; - background-position: center; - position: absolute; - right: 0px; - } - - .color-helper { - color: #000 !important; - -webkit-text-fill-color: #000 !important; - } - } - - .btn_full, .btn_full_blue { - display: block; - border-bottom: 2px solid black !important; - width: 100% !important; - border-radius: 0; - padding-bottom: 18px !important; - } - - .btn_full { - color: #000; - } - - .btn_full_blue { - color: #2646FF; - } - - .description_title, .description { - font-size: 14px; - font-weight: bold; - margin-bottom: 12px; - } - - .description { - font-weight: unset; - padding-top: 0; - margin-bottom: 29px; - opacity: 0.7; - } - - .description_helper { - margin-bottom: 53px; - } - - &.block-1 { - padding: 20px 0px 29px 0px; - - .btn-toolbar { - button { - color: #B2B2B2; - - @media (min-width: 768px) { - &:hover { - color: black; - } - } - - &.btn-active { - color: black; - } - } - } - - .btnLoginPhone { - width: 104px; - margin-top: 17px; - opacity: 0.4; - } - - .right-icon { - background-image: url('../images/btn-active-next.svg'); - top: 0px; - } - - .right-icon-blue { - background-image: url('../images/btn-active-next-blue.svg'); - top: 0px; - } - - .iti__selected-flag { - transition: background-color 0.2s; - } - - input { - background-color: #F2F2F6; - border: none; - border-bottom: 2px solid #2646FF; - border-radius: 0; - border-top-left-radius: 9px; - border-top-right-radius: 9px; - outline: none; - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; - color: #B2B2B2; - padding: 18px 50px 17px; - font-size: 16px; - width: '100%'; - transition: background-color 0.2s; - margin-left: 4px!important; - - &:focus { - outline: none; - -webkit-box-shadow: none; - box-shadow: none; - } - - &:hover { - background-color: #f2f2f6; - } - } - - input:-webkit-autofill, input:-webkit-autofill:hover, input:-webkit-autofill:focus, input:-webkit-autofill:active { - -webkit-box-shadow: 0 0 0 30px #000 inset !important; - -webkit-text-fill-color: #fff !important; - } - - ::placeholder { - color: #2647ff92; - font-weight: 300; - } - } - - &.block-active { - .btn_full { - color: #2646FF; - opacity: 1; - - .right-icon { - background-image: url('../images/btn-active-next-blue.svg'); - color: #2646FF; - } - } - - .iti--separate-dial-code .iti__selected-dial-code { - color: #fff; - } - - .iti__selected-dial-code { - color: #fff; - } - - .iti__selected-flag { - background-color: #000 !important; - } - - input { - background-color: black !important; - -ms-background-color: #000; - -mox-background-color: #000; - -o-background-color: #000; - color: white; - transition: background-color 0.2s; - - // border-bottom: 2px solid #2646FF; - &:hover { - background-color: #000; - -ms-background-color: #000; - -mox-background-color: #000; - -o-background-color: #000; - } - } - } - - &.block-2 { - padding: 20px 0px 29px 0px; - - button { - padding: 20px 0 20px 0px; - } - - .right-icon { - background-image: url('../images/btn-active-next-blue.svg'); - } - } - } -} - -@media (min-width: 576px) { - #login { - .form-login { - max-width: 580px; - } - } - - body { - background-color: white; - height: 100vh !important; - } - - .block-page { - // border: 1px solid #000000; - border-bottom: 0px; - margin-top: 80px; - } -} - -@media (max-width: 576px) { - .container { - padding-right: 0px; - padding-left: 0px; - } - - #login { - .form-login { - width: 100%; - } - } - - .block-page { - border-bottom: 0px; - width: 100%; - margin: 43px auto 0; - } -} - -.row-item { - padding-left: 15px; - padding-right: 15px; - transition: background-color 0.1s; - - @media (max-width: 576px) { - border-bottom: none; - padding-left: 20px; - padding-right: 20px; - } -} - -.row-item:first-child:hover { - background-color: unset; -} - -#input_alert, #error_alert, .error-popup, #phone_invalid_alert { - .modal-dialog { - position: absolute; - top: -110px; - left: 50%; - transform: translate(-50%); - - @media (max-width: 576px) { - margin: unset; - width: 70%; - margin-top: 20px; - } - - .modal-header { - margin: auto; - } - } -} - -#input_alert.show, #error_alert.show, .error-popup.show, #phone_invalid_alert.show { - .modal-dialog { - top: 0; - -webkit-transition: top 0.3s ease-out; - -moz-transition: top 0.3s ease-out; - -o-transition: top 0.3s ease-out; - transition: top 0.3s ease-out; - } -} - -#app_info { - @media (max-width: 576px) { - &.show .modal-dialog { - bottom: 0; - } - } - - .modal-dialog { - position: absolute; - top: 27%; - left: 50%; - transform: translate(-50%, -22%); - max-width: unset; - margin: 0; - - @media (max-width: 576px) { - top: unset; - left: 0; - bottom: -300px; - transform: unset; - width: 100%; - height: 70vh; - margin: 0; - -webkit-transition: bottom 0.3s ease-out; - -moz-transition: bottom 0.3s ease-out; - -o-transition: bottom 0.3s ease-out; - transition: bottom 0.3s ease-out; - } - - .modal-content { - border: 1px solid #000; - width: 630px; - height: 545px; - - @media (max-width: 576px) { - border: 0; - height: 100%; - width: 100%; - } - - .modal-header { - padding: 29px 20px 0; - border-bottom: unset; - - .close { - opacity: 1; - - .right-icon { - background-image: url('../images/close_btn.svg'); - width: 24px; - height: 24px; - background-repeat: no-repeat; - background-position: center; - position: absolute; - right: 38px; - top: 30px; - - @media (max-width: 576px) { - right: 26px; - } - } - } - } - - .modal-body { - padding: 30px 20px; - font-size: 14px; - - .developer-link { - position: absolute; - bottom: 30px; - left: 20px; - - a { - color: #000; - font-size: 14px; - text-decoration: underline; - } - } - } - } - } -} - -button.close:focus { - outline: none; -} - -@media (max-width: 576px) { - .block-page { - border: unset; - } -} - -.wrapper_response { - border: 1px solid #000; - padding: 30px; - width: 630px; - height: 565px; - border-radius: 0; - position: absolute; - top: 27%; - left: 50%; - transform: translate(-50%, -22%); - - @media (max-width: 576px) { - border: 0; - height: 100%; - width: 100%; - padding: 30px 15px; - position: unset; - transform: unset; - } - - .response-block { - width: 100%; - - .wrapper_title { - margin-bottom: 30px; - } - - .close { - opacity: 1; - - .right-icon { - background-image: url('../images/close_btn.svg'); - width: 24px; - height: 24px; - background-repeat: no-repeat; - background-position: center; - position: absolute; - right: 38px; - top: 30px; - - @media (max-width: 576px) { - right: 26px; - } - } - } - } - - .footer_response { - opacity: 0.6; - font-size: 14px; - - @media (max-width: 576px) { - font-size: 12px; - } - } -} - -#account-form-container { - .form-login { - width: 90%; - top: 30%; - } -} - -/* - modal windows -*/ -.modal_info { - .modal-dialog { - bottom: 0px; - position: absolute; - width: 100%; - max-width: 100%; - margin: 0px auto; - - .modal-content { - max-width: 400px; - margin: 0px auto; - - @media (max-width: 768px) { - max-width: 100%; - margin: 0px auto; - border-top-left-radius: 30px; - border-top-right-radius: 30px; - } - } - } -} - -.info-icon { - margin: auto; - text-align: right; - padding: 0 18px; - font-size: 14px; - cursor: pointer; - - &:hover { - text-decoration: underline; - } - - @media (max-width: 576px) { - right: 20px; - } -} - -#select { - position: absolute; - text-align: center; - right: 0; - font-size: 14px; - font-weight: bold; - border: none; - background-color: #fff; - - &:focus-visible { - outline: none; - } -} - -.privacy-link { - position: relative; - width: 610px; - margin: auto; - text-align: left; - padding-bottom: 55px; - padding-left: 16px; - - @media (max-width: 576px) { - width: 100%; - padding-left: 20px; - padding-bottom: 20px; - } - - a { - color: #000; - font-size: 14px; - } -} - -.center-vertical { - position: relative; - - form { - top: 40%; - margin: auto; - position: relative; - -webkit-transform: translateY(-50%); - -ms-transform: translateY(-50%); - transform: translateY(-50%); - } -} - -.margin_helper { - margin-bottom: 0; -} - -.json-pretty { - padding-left: 0px; - padding-right: 30px; - color: #000; - width: 90%; - font-size: 15px; - - @media (max-width: 576px) { - width: 100%; - padding-right: 0; - } -} - -.json-selected { - background-color: rgba(139, 191, 228, 0.19999999999999996); -} - -.json-string { - color: #000; -} - -.json-key { - color: #2646FF; -} - -.json-boolean { - color: #99c794; -} - -.json-number { - color: #99c794; -} - -.wrapper-loader { - position: fixed; - top: 0; - left: 0; - bottom: 0; - right: 0; - width: 100%; - background-color: #fff; - display: none; - - &.show { - display: block; - z-index: 9; - } - - .loader { - display: inline-block; - width: 30px; - height: 30px; - border: 4px solid #000; - animation: loader 2s infinite ease; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - - @media (max-width: 576px) { - left: 47%; - top: 47%; - } - - .loader-inner { - vertical-align: top; - display: inline-block; - width: 100%; - background-color: #000; - animation: loader-inner 2s infinite ease-in; - -webkit-vertical-align: top; - -webkit-display: inline-block; - -webkit-width: 100%; - -webkit-background-color: #000; - -webkit-animation: loader-inner 2s infinite ease-in; - } - } - - @keyframes loader { - 0% { - transform: rotate(0deg); - } - - 25% { - transform: rotate(180deg); - } - - 50% { - transform: rotate(180deg); - } - - 75% { - transform: rotate(360deg); - } - - 100% { - transform: rotate(360deg); - } - } - - @-webkit-; - - @keyframes loader { - 0% { - transform: rotate(0deg); - } - - 25% { - transform: rotate(180deg); - } - - 50% { - transform: rotate(180deg); - } - - 75% { - transform: rotate(360deg); - } - - 100% { - transform: rotate(360deg); - } - } - - @keyframes loader-inner { - 0% { - height: 0%; - } - - 25% { - height: 0%; - } - - 50% { - height: 100%; - } - - 75% { - height: 100%; - } - - 100% { - height: 0%; - } - } - - @-webkit-; - - @keyframes loader-inner { - 0% { - height: 0%; - } - - 25% { - height: 0%; - } - - 50% { - height: 100%; - } - - 75% { - height: 100%; - } - - 100% { - height: 0%; - } - } -} - -@keyframes fadeIn { - 0% { - opacity: 0; - } - - 100% { - opacity: 1; - } -} - -@-webkit-; - -@keyframes fadeIn { - 0% { - opacity: 0; - } - - 100% { - opacity: 1; - } -} - -@-moz-; - -@keyframes fadeIn { - 0% { - opacity: 0; - } - - 100% { - opacity: 1; - } -} - -// qrcode -.swal2-container.swal2-center>.swal2-popup.qrcode-popup { - width: 630px; - height: 545px; - border-radius: 0; - grid-row: unset !important; - top: 13%; - border: 1px solid #000; -} - -.qrcode-title { - position: absolute !important; - bottom: 30px; - left: 50%; - transform: translate(-50%, 0); - font-size: 14px; - color: #1A1A1A; -} - -.qrcode-popup .swal2-html-container { - margin-top: -60px !important; -} - -.qrcode-img { - margin-top: 80px; - - img { - margin: auto; - max-width: 100%; - max-height: 100%; - } -} - -.swal2-close { - color: #1A1A1A !important; - overflow: unset !important; - outline: none !important; - font-family: 'Inter' !important; - font-weight: 200; - padding-top: 30px !important; - padding-right: 38px !important; - - &:focus { - box-shadow: none !important; - } -} - -.qrcode-popup .swal2-title { - color: #1A1A1A !important; - padding: 0 !important; - height: 50px; - position: absolute !important; - top: 30px; - left: 26px; - font-size: 20px !important; -} - -.swal2-container.swal2-center>.swal2-popup.sms-otp-popup { - width: 460px; - max-width: 92vw; - height: auto; - border-radius: 0; - border: 1px solid #000; - padding: 28px 28px 24px; - top: unset; - grid-row: unset !important; -} - -.sms-otp-popup .swal2-title { - position: static !important; - height: auto; - padding: 0 !important; - margin: 0 0 18px !important; - font-size: 20px !important; - line-height: 1.2; - color: #1A1A1A !important; - text-align: left; -} - -.sms-otp-popup .swal2-html-container { - margin: 0 !important; -} - -.sms-otp-popup .swal2-input { - width: 100% !important; - margin: 0 !important; - height: 56px; - border: none; - border-bottom: 2px solid #2646ff; - border-radius: 9px 9px 0 0; - background-color: #f2f2f6; - box-shadow: none !important; - color: #1A1A1A; - font-size: 16px; - padding: 0 18px; -} - -.sms-otp-popup .swal2-input:focus { - border-bottom-color: #2646ff; - box-shadow: none !important; -} - -.sms-otp-popup .swal2-actions { - width: 100%; - margin: 24px 0 0 !important; - justify-content: flex-end; - gap: 12px; -} - -.sms-otp-popup .swal2-styled { - margin: 0 !important; - padding: 14px 26px; - border-radius: 0; - font-size: 16px; - font-weight: 700; - box-shadow: none !important; -} - -.sms-otp-popup .swal2-confirm { - background-color: #2646ff !important; -} - -.sms-otp-popup .swal2-cancel { - background-color: #7d8793 !important; -} - -@media (max-width: 576px) { - .swal2-container.swal2-center>.swal2-popup.qrcode-popup { - width: calc(100vw - 24px); - height: auto; - top: unset; - padding-bottom: 32px; - } - - .sms-otp-popup .swal2-actions { - justify-content: stretch; - } - - .sms-otp-popup .swal2-styled { - flex: 1 1 0; - } -} - -.iti { - display: block !important; -} - -.iti__selected-flag { - background-color: #f2f2f6 !important; - height: 98% !important; - border-radius: 11px; - padding: 0 6px 1px 8px !important; -} - -.iti--separate-dial-code .iti__selected-dial-code { - color: #2646FF !important; -} - -.iti__selected-dial-code { - color: #2646FF; -} - -.modal-dialog-success { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 80%; - - .modal-content-success { - border: none; - - .description { - float: left; - font-size: 14px; - } - - .text-success { - color: #000 !important; - font-size: 24px; - float: left; - height: 28px; - width: 100%; - text-align: left; - } - - .success { - background-image: url('../images/check.svg'); - width: 23px; - height: 17px; - position: absolute; - right: 0; - top: 12px; - } - } -} - -.modal-dialog-error { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 80%; - - .modal-content-error { - border: none; - - .description { - float: left; - font-size: 14px; - } - - .text-error { - color: #000 !important; - font-size: 24px; - float: left; - height: 28px; - width: 100%; - text-align: left; - } - - .error { - background-image: url('../images/error.svg'); - width: 19px; - height: 19px; - position: absolute; - right: 0; - top: 12px; - } - } -} - -.viettel-legal { - font-size: 12px; - max-width: 580px; - margin: auto; - padding: 10px; - display: none; - - @media (max-width: 576px) { - padding: 20px; - } -} - -.consent-page { - margin-top: 100px; - - iframe { - width: 100%; - height: 400px; - overflow: auto; - border: 0px; - } -} - -.iti__flag-container .iti__selected-flag .iti__flag.iti__ww { - box-shadow: unset; -} - -.iti__flag.iti__ww { - background-position: unset !important; - background-repeat: no-repeat; - box-shadow: unset; - background-color: unset; - background-image: url('../images/logo.ico'); - background-size: 21px 21px; - height: 20px; - width: 22px; -} - -.iti__country-list { - .iti__flag.iti__ww { - background-image: url('../images/logo.ico'); - background-size: 21px 21px; - background-position: 0px; - height: 20px; - } -} - -.iti__flag-container .iti__selected-flag { - .iti__flag.iti__ww { - background-image: url('../images/logo.ico'); - background-position: 0px; - background-size: 21px 21px; - height: 20px; - } -} - -.iti__search-input { - border-bottom: unset !important; - border-radius: 0 !important; - padding: 17px 17px 17px 8px !important; - background-color: #fff !important; - - &:focus-visible { - outline: none; - } -} 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 0000000..b9e3cb3 Binary files /dev/null and b/public/fonts/Inter-Bold.woff2 differ diff --git a/public/fonts/Inter-Medium.woff2 b/public/fonts/Inter-Medium.woff2 new file mode 100644 index 0000000..fdfdcc6 Binary files /dev/null and b/public/fonts/Inter-Medium.woff2 differ diff --git a/public/fonts/Inter-Regular.woff2 b/public/fonts/Inter-Regular.woff2 new file mode 100644 index 0000000..2bcd222 Binary files /dev/null and b/public/fonts/Inter-Regular.woff2 differ 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/public/js/lib/bootstrap.min.js b/public/js/lib/bootstrap.min.js deleted file mode 100644 index bd3bd40..0000000 --- a/public/js/lib/bootstrap.min.js +++ /dev/null @@ -1,6 +0,0 @@ -/*! - * Bootstrap v4.0.0 (https://getbootstrap.com) - * Copyright 2011-2018 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - */ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("jquery"),require("popper.js")):"function"==typeof define&&define.amd?define(["exports","jquery","popper.js"],e):e(t.bootstrap={},t.jQuery,t.Popper)}(this,function(t,e,n){"use strict";function i(t,e){for(var n=0;n0?i:null}catch(t){return null}},reflow:function(t){return t.offsetHeight},triggerTransitionEnd:function(n){t(n).trigger(e.end)},supportsTransitionEnd:function(){return Boolean(e)},isElement:function(t){return(t[0]||t).nodeType},typeCheckConfig:function(t,e,n){for(var s in n)if(Object.prototype.hasOwnProperty.call(n,s)){var r=n[s],o=e[s],a=o&&i.isElement(o)?"element":(l=o,{}.toString.call(l).match(/\s([a-zA-Z]+)/)[1].toLowerCase());if(!new RegExp(r).test(a))throw new Error(t.toUpperCase()+': Option "'+s+'" provided type "'+a+'" but expected type "'+r+'".')}var l}};return e=("undefined"==typeof window||!window.QUnit)&&{end:"transitionend"},t.fn.emulateTransitionEnd=n,i.supportsTransitionEnd()&&(t.event.special[i.TRANSITION_END]={bindType:e.end,delegateType:e.end,handle:function(e){if(t(e.target).is(this))return e.handleObj.handler.apply(this,arguments)}}),i}(e),L=(a="alert",h="."+(l="bs.alert"),c=(o=e).fn[a],u={CLOSE:"close"+h,CLOSED:"closed"+h,CLICK_DATA_API:"click"+h+".data-api"},f="alert",d="fade",_="show",g=function(){function t(t){this._element=t}var e=t.prototype;return e.close=function(t){t=t||this._element;var e=this._getRootElement(t);this._triggerCloseEvent(e).isDefaultPrevented()||this._removeElement(e)},e.dispose=function(){o.removeData(this._element,l),this._element=null},e._getRootElement=function(t){var e=P.getSelectorFromElement(t),n=!1;return e&&(n=o(e)[0]),n||(n=o(t).closest("."+f)[0]),n},e._triggerCloseEvent=function(t){var e=o.Event(u.CLOSE);return o(t).trigger(e),e},e._removeElement=function(t){var e=this;o(t).removeClass(_),P.supportsTransitionEnd()&&o(t).hasClass(d)?o(t).one(P.TRANSITION_END,function(n){return e._destroyElement(t,n)}).emulateTransitionEnd(150):this._destroyElement(t)},e._destroyElement=function(t){o(t).detach().trigger(u.CLOSED).remove()},t._jQueryInterface=function(e){return this.each(function(){var n=o(this),i=n.data(l);i||(i=new t(this),n.data(l,i)),"close"===e&&i[e](this)})},t._handleDismiss=function(t){return function(e){e&&e.preventDefault(),t.close(this)}},s(t,null,[{key:"VERSION",get:function(){return"4.0.0"}}]),t}(),o(document).on(u.CLICK_DATA_API,'[data-dismiss="alert"]',g._handleDismiss(new g)),o.fn[a]=g._jQueryInterface,o.fn[a].Constructor=g,o.fn[a].noConflict=function(){return o.fn[a]=c,g._jQueryInterface},g),R=(m="button",E="."+(v="bs.button"),T=".data-api",y=(p=e).fn[m],C="active",I="btn",A="focus",b='[data-toggle^="button"]',D='[data-toggle="buttons"]',S="input",w=".active",N=".btn",O={CLICK_DATA_API:"click"+E+T,FOCUS_BLUR_DATA_API:"focus"+E+T+" blur"+E+T},k=function(){function t(t){this._element=t}var e=t.prototype;return e.toggle=function(){var t=!0,e=!0,n=p(this._element).closest(D)[0];if(n){var i=p(this._element).find(S)[0];if(i){if("radio"===i.type)if(i.checked&&p(this._element).hasClass(C))t=!1;else{var s=p(n).find(w)[0];s&&p(s).removeClass(C)}if(t){if(i.hasAttribute("disabled")||n.hasAttribute("disabled")||i.classList.contains("disabled")||n.classList.contains("disabled"))return;i.checked=!p(this._element).hasClass(C),p(i).trigger("change")}i.focus(),e=!1}}e&&this._element.setAttribute("aria-pressed",!p(this._element).hasClass(C)),t&&p(this._element).toggleClass(C)},e.dispose=function(){p.removeData(this._element,v),this._element=null},t._jQueryInterface=function(e){return this.each(function(){var n=p(this).data(v);n||(n=new t(this),p(this).data(v,n)),"toggle"===e&&n[e]()})},s(t,null,[{key:"VERSION",get:function(){return"4.0.0"}}]),t}(),p(document).on(O.CLICK_DATA_API,b,function(t){t.preventDefault();var e=t.target;p(e).hasClass(I)||(e=p(e).closest(N)),k._jQueryInterface.call(p(e),"toggle")}).on(O.FOCUS_BLUR_DATA_API,b,function(t){var e=p(t.target).closest(N)[0];p(e).toggleClass(A,/^focus(in)?$/.test(t.type))}),p.fn[m]=k._jQueryInterface,p.fn[m].Constructor=k,p.fn[m].noConflict=function(){return p.fn[m]=y,k._jQueryInterface},k),j=function(t){var e="carousel",n="bs.carousel",i="."+n,o=t.fn[e],a={interval:5e3,keyboard:!0,slide:!1,pause:"hover",wrap:!0},l={interval:"(number|boolean)",keyboard:"boolean",slide:"(boolean|string)",pause:"(string|boolean)",wrap:"boolean"},h="next",c="prev",u="left",f="right",d={SLIDE:"slide"+i,SLID:"slid"+i,KEYDOWN:"keydown"+i,MOUSEENTER:"mouseenter"+i,MOUSELEAVE:"mouseleave"+i,TOUCHEND:"touchend"+i,LOAD_DATA_API:"load"+i+".data-api",CLICK_DATA_API:"click"+i+".data-api"},_="carousel",g="active",p="slide",m="carousel-item-right",v="carousel-item-left",E="carousel-item-next",T="carousel-item-prev",y={ACTIVE:".active",ACTIVE_ITEM:".active.carousel-item",ITEM:".carousel-item",NEXT_PREV:".carousel-item-next, .carousel-item-prev",INDICATORS:".carousel-indicators",DATA_SLIDE:"[data-slide], [data-slide-to]",DATA_RIDE:'[data-ride="carousel"]'},C=function(){function o(e,n){this._items=null,this._interval=null,this._activeElement=null,this._isPaused=!1,this._isSliding=!1,this.touchTimeout=null,this._config=this._getConfig(n),this._element=t(e)[0],this._indicatorsElement=t(this._element).find(y.INDICATORS)[0],this._addEventListeners()}var C=o.prototype;return C.next=function(){this._isSliding||this._slide(h)},C.nextWhenVisible=function(){!document.hidden&&t(this._element).is(":visible")&&"hidden"!==t(this._element).css("visibility")&&this.next()},C.prev=function(){this._isSliding||this._slide(c)},C.pause=function(e){e||(this._isPaused=!0),t(this._element).find(y.NEXT_PREV)[0]&&P.supportsTransitionEnd()&&(P.triggerTransitionEnd(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null},C.cycle=function(t){t||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config.interval&&!this._isPaused&&(this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))},C.to=function(e){var n=this;this._activeElement=t(this._element).find(y.ACTIVE_ITEM)[0];var i=this._getItemIndex(this._activeElement);if(!(e>this._items.length-1||e<0))if(this._isSliding)t(this._element).one(d.SLID,function(){return n.to(e)});else{if(i===e)return this.pause(),void this.cycle();var s=e>i?h:c;this._slide(s,this._items[e])}},C.dispose=function(){t(this._element).off(i),t.removeData(this._element,n),this._items=null,this._config=null,this._element=null,this._interval=null,this._isPaused=null,this._isSliding=null,this._activeElement=null,this._indicatorsElement=null},C._getConfig=function(t){return t=r({},a,t),P.typeCheckConfig(e,t,l),t},C._addEventListeners=function(){var e=this;this._config.keyboard&&t(this._element).on(d.KEYDOWN,function(t){return e._keydown(t)}),"hover"===this._config.pause&&(t(this._element).on(d.MOUSEENTER,function(t){return e.pause(t)}).on(d.MOUSELEAVE,function(t){return e.cycle(t)}),"ontouchstart"in document.documentElement&&t(this._element).on(d.TOUCHEND,function(){e.pause(),e.touchTimeout&&clearTimeout(e.touchTimeout),e.touchTimeout=setTimeout(function(t){return e.cycle(t)},500+e._config.interval)}))},C._keydown=function(t){if(!/input|textarea/i.test(t.target.tagName))switch(t.which){case 37:t.preventDefault(),this.prev();break;case 39:t.preventDefault(),this.next()}},C._getItemIndex=function(e){return this._items=t.makeArray(t(e).parent().find(y.ITEM)),this._items.indexOf(e)},C._getItemByDirection=function(t,e){var n=t===h,i=t===c,s=this._getItemIndex(e),r=this._items.length-1;if((i&&0===s||n&&s===r)&&!this._config.wrap)return e;var o=(s+(t===c?-1:1))%this._items.length;return-1===o?this._items[this._items.length-1]:this._items[o]},C._triggerSlideEvent=function(e,n){var i=this._getItemIndex(e),s=this._getItemIndex(t(this._element).find(y.ACTIVE_ITEM)[0]),r=t.Event(d.SLIDE,{relatedTarget:e,direction:n,from:s,to:i});return t(this._element).trigger(r),r},C._setActiveIndicatorElement=function(e){if(this._indicatorsElement){t(this._indicatorsElement).find(y.ACTIVE).removeClass(g);var n=this._indicatorsElement.children[this._getItemIndex(e)];n&&t(n).addClass(g)}},C._slide=function(e,n){var i,s,r,o=this,a=t(this._element).find(y.ACTIVE_ITEM)[0],l=this._getItemIndex(a),c=n||a&&this._getItemByDirection(e,a),_=this._getItemIndex(c),C=Boolean(this._interval);if(e===h?(i=v,s=E,r=u):(i=m,s=T,r=f),c&&t(c).hasClass(g))this._isSliding=!1;else if(!this._triggerSlideEvent(c,r).isDefaultPrevented()&&a&&c){this._isSliding=!0,C&&this.pause(),this._setActiveIndicatorElement(c);var I=t.Event(d.SLID,{relatedTarget:c,direction:r,from:l,to:_});P.supportsTransitionEnd()&&t(this._element).hasClass(p)?(t(c).addClass(s),P.reflow(c),t(a).addClass(i),t(c).addClass(i),t(a).one(P.TRANSITION_END,function(){t(c).removeClass(i+" "+s).addClass(g),t(a).removeClass(g+" "+s+" "+i),o._isSliding=!1,setTimeout(function(){return t(o._element).trigger(I)},0)}).emulateTransitionEnd(600)):(t(a).removeClass(g),t(c).addClass(g),this._isSliding=!1,t(this._element).trigger(I)),C&&this.cycle()}},o._jQueryInterface=function(e){return this.each(function(){var i=t(this).data(n),s=r({},a,t(this).data());"object"==typeof e&&(s=r({},s,e));var l="string"==typeof e?e:s.slide;if(i||(i=new o(this,s),t(this).data(n,i)),"number"==typeof e)i.to(e);else if("string"==typeof l){if("undefined"==typeof i[l])throw new TypeError('No method named "'+l+'"');i[l]()}else s.interval&&(i.pause(),i.cycle())})},o._dataApiClickHandler=function(e){var i=P.getSelectorFromElement(this);if(i){var s=t(i)[0];if(s&&t(s).hasClass(_)){var a=r({},t(s).data(),t(this).data()),l=this.getAttribute("data-slide-to");l&&(a.interval=!1),o._jQueryInterface.call(t(s),a),l&&t(s).data(n).to(l),e.preventDefault()}}},s(o,null,[{key:"VERSION",get:function(){return"4.0.0"}},{key:"Default",get:function(){return a}}]),o}();return t(document).on(d.CLICK_DATA_API,y.DATA_SLIDE,C._dataApiClickHandler),t(window).on(d.LOAD_DATA_API,function(){t(y.DATA_RIDE).each(function(){var e=t(this);C._jQueryInterface.call(e,e.data())})}),t.fn[e]=C._jQueryInterface,t.fn[e].Constructor=C,t.fn[e].noConflict=function(){return t.fn[e]=o,C._jQueryInterface},C}(e),H=function(t){var e="collapse",n="bs.collapse",i="."+n,o=t.fn[e],a={toggle:!0,parent:""},l={toggle:"boolean",parent:"(string|element)"},h={SHOW:"show"+i,SHOWN:"shown"+i,HIDE:"hide"+i,HIDDEN:"hidden"+i,CLICK_DATA_API:"click"+i+".data-api"},c="show",u="collapse",f="collapsing",d="collapsed",_="width",g="height",p={ACTIVES:".show, .collapsing",DATA_TOGGLE:'[data-toggle="collapse"]'},m=function(){function i(e,n){this._isTransitioning=!1,this._element=e,this._config=this._getConfig(n),this._triggerArray=t.makeArray(t('[data-toggle="collapse"][href="#'+e.id+'"],[data-toggle="collapse"][data-target="#'+e.id+'"]'));for(var i=t(p.DATA_TOGGLE),s=0;s0&&(this._selector=o,this._triggerArray.push(r))}this._parent=this._config.parent?this._getParent():null,this._config.parent||this._addAriaAndCollapsedClass(this._element,this._triggerArray),this._config.toggle&&this.toggle()}var o=i.prototype;return o.toggle=function(){t(this._element).hasClass(c)?this.hide():this.show()},o.show=function(){var e,s,r=this;if(!this._isTransitioning&&!t(this._element).hasClass(c)&&(this._parent&&0===(e=t.makeArray(t(this._parent).find(p.ACTIVES).filter('[data-parent="'+this._config.parent+'"]'))).length&&(e=null),!(e&&(s=t(e).not(this._selector).data(n))&&s._isTransitioning))){var o=t.Event(h.SHOW);if(t(this._element).trigger(o),!o.isDefaultPrevented()){e&&(i._jQueryInterface.call(t(e).not(this._selector),"hide"),s||t(e).data(n,null));var a=this._getDimension();t(this._element).removeClass(u).addClass(f),this._element.style[a]=0,this._triggerArray.length>0&&t(this._triggerArray).removeClass(d).attr("aria-expanded",!0),this.setTransitioning(!0);var l=function(){t(r._element).removeClass(f).addClass(u).addClass(c),r._element.style[a]="",r.setTransitioning(!1),t(r._element).trigger(h.SHOWN)};if(P.supportsTransitionEnd()){var _="scroll"+(a[0].toUpperCase()+a.slice(1));t(this._element).one(P.TRANSITION_END,l).emulateTransitionEnd(600),this._element.style[a]=this._element[_]+"px"}else l()}}},o.hide=function(){var e=this;if(!this._isTransitioning&&t(this._element).hasClass(c)){var n=t.Event(h.HIDE);if(t(this._element).trigger(n),!n.isDefaultPrevented()){var i=this._getDimension();if(this._element.style[i]=this._element.getBoundingClientRect()[i]+"px",P.reflow(this._element),t(this._element).addClass(f).removeClass(u).removeClass(c),this._triggerArray.length>0)for(var s=0;s0&&t(n).toggleClass(d,!i).attr("aria-expanded",i)}},i._getTargetFromElement=function(e){var n=P.getSelectorFromElement(e);return n?t(n)[0]:null},i._jQueryInterface=function(e){return this.each(function(){var s=t(this),o=s.data(n),l=r({},a,s.data(),"object"==typeof e&&e);if(!o&&l.toggle&&/show|hide/.test(e)&&(l.toggle=!1),o||(o=new i(this,l),s.data(n,o)),"string"==typeof e){if("undefined"==typeof o[e])throw new TypeError('No method named "'+e+'"');o[e]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.0.0"}},{key:"Default",get:function(){return a}}]),i}();return t(document).on(h.CLICK_DATA_API,p.DATA_TOGGLE,function(e){"A"===e.currentTarget.tagName&&e.preventDefault();var i=t(this),s=P.getSelectorFromElement(this);t(s).each(function(){var e=t(this),s=e.data(n)?"toggle":i.data();m._jQueryInterface.call(e,s)})}),t.fn[e]=m._jQueryInterface,t.fn[e].Constructor=m,t.fn[e].noConflict=function(){return t.fn[e]=o,m._jQueryInterface},m}(e),W=function(t){var e="dropdown",i="bs.dropdown",o="."+i,a=".data-api",l=t.fn[e],h=new RegExp("38|40|27"),c={HIDE:"hide"+o,HIDDEN:"hidden"+o,SHOW:"show"+o,SHOWN:"shown"+o,CLICK:"click"+o,CLICK_DATA_API:"click"+o+a,KEYDOWN_DATA_API:"keydown"+o+a,KEYUP_DATA_API:"keyup"+o+a},u="disabled",f="show",d="dropup",_="dropright",g="dropleft",p="dropdown-menu-right",m="dropdown-menu-left",v="position-static",E='[data-toggle="dropdown"]',T=".dropdown form",y=".dropdown-menu",C=".navbar-nav",I=".dropdown-menu .dropdown-item:not(.disabled)",A="top-start",b="top-end",D="bottom-start",S="bottom-end",w="right-start",N="left-start",O={offset:0,flip:!0,boundary:"scrollParent"},k={offset:"(number|string|function)",flip:"boolean",boundary:"(string|element)"},L=function(){function a(t,e){this._element=t,this._popper=null,this._config=this._getConfig(e),this._menu=this._getMenuElement(),this._inNavbar=this._detectNavbar(),this._addEventListeners()}var l=a.prototype;return l.toggle=function(){if(!this._element.disabled&&!t(this._element).hasClass(u)){var e=a._getParentFromElement(this._element),i=t(this._menu).hasClass(f);if(a._clearMenus(),!i){var s={relatedTarget:this._element},r=t.Event(c.SHOW,s);if(t(e).trigger(r),!r.isDefaultPrevented()){if(!this._inNavbar){if("undefined"==typeof n)throw new TypeError("Bootstrap dropdown require Popper.js (https://popper.js.org)");var o=this._element;t(e).hasClass(d)&&(t(this._menu).hasClass(m)||t(this._menu).hasClass(p))&&(o=e),"scrollParent"!==this._config.boundary&&t(e).addClass(v),this._popper=new n(o,this._menu,this._getPopperConfig())}"ontouchstart"in document.documentElement&&0===t(e).closest(C).length&&t("body").children().on("mouseover",null,t.noop),this._element.focus(),this._element.setAttribute("aria-expanded",!0),t(this._menu).toggleClass(f),t(e).toggleClass(f).trigger(t.Event(c.SHOWN,s))}}}},l.dispose=function(){t.removeData(this._element,i),t(this._element).off(o),this._element=null,this._menu=null,null!==this._popper&&(this._popper.destroy(),this._popper=null)},l.update=function(){this._inNavbar=this._detectNavbar(),null!==this._popper&&this._popper.scheduleUpdate()},l._addEventListeners=function(){var e=this;t(this._element).on(c.CLICK,function(t){t.preventDefault(),t.stopPropagation(),e.toggle()})},l._getConfig=function(n){return n=r({},this.constructor.Default,t(this._element).data(),n),P.typeCheckConfig(e,n,this.constructor.DefaultType),n},l._getMenuElement=function(){if(!this._menu){var e=a._getParentFromElement(this._element);this._menu=t(e).find(y)[0]}return this._menu},l._getPlacement=function(){var e=t(this._element).parent(),n=D;return e.hasClass(d)?(n=A,t(this._menu).hasClass(p)&&(n=b)):e.hasClass(_)?n=w:e.hasClass(g)?n=N:t(this._menu).hasClass(p)&&(n=S),n},l._detectNavbar=function(){return t(this._element).closest(".navbar").length>0},l._getPopperConfig=function(){var t=this,e={};return"function"==typeof this._config.offset?e.fn=function(e){return e.offsets=r({},e.offsets,t._config.offset(e.offsets)||{}),e}:e.offset=this._config.offset,{placement:this._getPlacement(),modifiers:{offset:e,flip:{enabled:this._config.flip},preventOverflow:{boundariesElement:this._config.boundary}}}},a._jQueryInterface=function(e){return this.each(function(){var n=t(this).data(i);if(n||(n=new a(this,"object"==typeof e?e:null),t(this).data(i,n)),"string"==typeof e){if("undefined"==typeof n[e])throw new TypeError('No method named "'+e+'"');n[e]()}})},a._clearMenus=function(e){if(!e||3!==e.which&&("keyup"!==e.type||9===e.which))for(var n=t.makeArray(t(E)),s=0;s0&&r--,40===e.which&&rdocument.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},p._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},p._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=t.left+t.right
',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent"},f="show",d="out",_={HIDE:"hide"+o,HIDDEN:"hidden"+o,SHOW:"show"+o,SHOWN:"shown"+o,INSERTED:"inserted"+o,CLICK:"click"+o,FOCUSIN:"focusin"+o,FOCUSOUT:"focusout"+o,MOUSEENTER:"mouseenter"+o,MOUSELEAVE:"mouseleave"+o},g="fade",p="show",m=".tooltip-inner",v=".arrow",E="hover",T="focus",y="click",C="manual",I=function(){function a(t,e){if("undefined"==typeof n)throw new TypeError("Bootstrap tooltips require Popper.js (https://popper.js.org)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var I=a.prototype;return I.enable=function(){this._isEnabled=!0},I.disable=function(){this._isEnabled=!1},I.toggleEnabled=function(){this._isEnabled=!this._isEnabled},I.toggle=function(e){if(this._isEnabled)if(e){var n=this.constructor.DATA_KEY,i=t(e.currentTarget).data(n);i||(i=new this.constructor(e.currentTarget,this._getDelegateConfig()),t(e.currentTarget).data(n,i)),i._activeTrigger.click=!i._activeTrigger.click,i._isWithActiveTrigger()?i._enter(null,i):i._leave(null,i)}else{if(t(this.getTipElement()).hasClass(p))return void this._leave(null,this);this._enter(null,this)}},I.dispose=function(){clearTimeout(this._timeout),t.removeData(this.element,this.constructor.DATA_KEY),t(this.element).off(this.constructor.EVENT_KEY),t(this.element).closest(".modal").off("hide.bs.modal"),this.tip&&t(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,this._activeTrigger=null,null!==this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},I.show=function(){var e=this;if("none"===t(this.element).css("display"))throw new Error("Please use show on visible elements");var i=t.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){t(this.element).trigger(i);var s=t.contains(this.element.ownerDocument.documentElement,this.element);if(i.isDefaultPrevented()||!s)return;var r=this.getTipElement(),o=P.getUID(this.constructor.NAME);r.setAttribute("id",o),this.element.setAttribute("aria-describedby",o),this.setContent(),this.config.animation&&t(r).addClass(g);var l="function"==typeof this.config.placement?this.config.placement.call(this,r,this.element):this.config.placement,h=this._getAttachment(l);this.addAttachmentClass(h);var c=!1===this.config.container?document.body:t(this.config.container);t(r).data(this.constructor.DATA_KEY,this),t.contains(this.element.ownerDocument.documentElement,this.tip)||t(r).appendTo(c),t(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new n(this.element,r,{placement:h,modifiers:{offset:{offset:this.config.offset},flip:{behavior:this.config.fallbackPlacement},arrow:{element:v},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){e._handlePopperPlacementChange(t)}}),t(r).addClass(p),"ontouchstart"in document.documentElement&&t("body").children().on("mouseover",null,t.noop);var u=function(){e.config.animation&&e._fixTransition();var n=e._hoverState;e._hoverState=null,t(e.element).trigger(e.constructor.Event.SHOWN),n===d&&e._leave(null,e)};P.supportsTransitionEnd()&&t(this.tip).hasClass(g)?t(this.tip).one(P.TRANSITION_END,u).emulateTransitionEnd(a._TRANSITION_DURATION):u()}},I.hide=function(e){var n=this,i=this.getTipElement(),s=t.Event(this.constructor.Event.HIDE),r=function(){n._hoverState!==f&&i.parentNode&&i.parentNode.removeChild(i),n._cleanTipClass(),n.element.removeAttribute("aria-describedby"),t(n.element).trigger(n.constructor.Event.HIDDEN),null!==n._popper&&n._popper.destroy(),e&&e()};t(this.element).trigger(s),s.isDefaultPrevented()||(t(i).removeClass(p),"ontouchstart"in document.documentElement&&t("body").children().off("mouseover",null,t.noop),this._activeTrigger[y]=!1,this._activeTrigger[T]=!1,this._activeTrigger[E]=!1,P.supportsTransitionEnd()&&t(this.tip).hasClass(g)?t(i).one(P.TRANSITION_END,r).emulateTransitionEnd(150):r(),this._hoverState="")},I.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},I.isWithContent=function(){return Boolean(this.getTitle())},I.addAttachmentClass=function(e){t(this.getTipElement()).addClass("bs-tooltip-"+e)},I.getTipElement=function(){return this.tip=this.tip||t(this.config.template)[0],this.tip},I.setContent=function(){var e=t(this.getTipElement());this.setElementContent(e.find(m),this.getTitle()),e.removeClass(g+" "+p)},I.setElementContent=function(e,n){var i=this.config.html;"object"==typeof n&&(n.nodeType||n.jquery)?i?t(n).parent().is(e)||e.empty().append(n):e.text(t(n).text()):e[i?"html":"text"](n)},I.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},I._getAttachment=function(t){return c[t.toUpperCase()]},I._setListeners=function(){var e=this;this.config.trigger.split(" ").forEach(function(n){if("click"===n)t(e.element).on(e.constructor.Event.CLICK,e.config.selector,function(t){return e.toggle(t)});else if(n!==C){var i=n===E?e.constructor.Event.MOUSEENTER:e.constructor.Event.FOCUSIN,s=n===E?e.constructor.Event.MOUSELEAVE:e.constructor.Event.FOCUSOUT;t(e.element).on(i,e.config.selector,function(t){return e._enter(t)}).on(s,e.config.selector,function(t){return e._leave(t)})}t(e.element).closest(".modal").on("hide.bs.modal",function(){return e.hide()})}),this.config.selector?this.config=r({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},I._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},I._enter=function(e,n){var i=this.constructor.DATA_KEY;(n=n||t(e.currentTarget).data(i))||(n=new this.constructor(e.currentTarget,this._getDelegateConfig()),t(e.currentTarget).data(i,n)),e&&(n._activeTrigger["focusin"===e.type?T:E]=!0),t(n.getTipElement()).hasClass(p)||n._hoverState===f?n._hoverState=f:(clearTimeout(n._timeout),n._hoverState=f,n.config.delay&&n.config.delay.show?n._timeout=setTimeout(function(){n._hoverState===f&&n.show()},n.config.delay.show):n.show())},I._leave=function(e,n){var i=this.constructor.DATA_KEY;(n=n||t(e.currentTarget).data(i))||(n=new this.constructor(e.currentTarget,this._getDelegateConfig()),t(e.currentTarget).data(i,n)),e&&(n._activeTrigger["focusout"===e.type?T:E]=!1),n._isWithActiveTrigger()||(clearTimeout(n._timeout),n._hoverState=d,n.config.delay&&n.config.delay.hide?n._timeout=setTimeout(function(){n._hoverState===d&&n.hide()},n.config.delay.hide):n.hide())},I._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},I._getConfig=function(n){return"number"==typeof(n=r({},this.constructor.Default,t(this.element).data(),n)).delay&&(n.delay={show:n.delay,hide:n.delay}),"number"==typeof n.title&&(n.title=n.title.toString()),"number"==typeof n.content&&(n.content=n.content.toString()),P.typeCheckConfig(e,n,this.constructor.DefaultType),n},I._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},I._cleanTipClass=function(){var e=t(this.getTipElement()),n=e.attr("class").match(l);null!==n&&n.length>0&&e.removeClass(n.join(""))},I._handlePopperPlacementChange=function(t){this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},I._fixTransition=function(){var e=this.getTipElement(),n=this.config.animation;null===e.getAttribute("x-placement")&&(t(e).removeClass(g),this.config.animation=!1,this.hide(),this.show(),this.config.animation=n)},a._jQueryInterface=function(e){return this.each(function(){var n=t(this).data(i),s="object"==typeof e&&e;if((n||!/dispose|hide/.test(e))&&(n||(n=new a(this,s),t(this).data(i,n)),"string"==typeof e)){if("undefined"==typeof n[e])throw new TypeError('No method named "'+e+'"');n[e]()}})},s(a,null,[{key:"VERSION",get:function(){return"4.0.0"}},{key:"Default",get:function(){return u}},{key:"NAME",get:function(){return e}},{key:"DATA_KEY",get:function(){return i}},{key:"Event",get:function(){return _}},{key:"EVENT_KEY",get:function(){return o}},{key:"DefaultType",get:function(){return h}}]),a}();return t.fn[e]=I._jQueryInterface,t.fn[e].Constructor=I,t.fn[e].noConflict=function(){return t.fn[e]=a,I._jQueryInterface},I}(e),x=function(t){var e="popover",n="bs.popover",i="."+n,o=t.fn[e],a=new RegExp("(^|\\s)bs-popover\\S+","g"),l=r({},U.Default,{placement:"right",trigger:"click",content:"",template:''}),h=r({},U.DefaultType,{content:"(string|element|function)"}),c="fade",u="show",f=".popover-header",d=".popover-body",_={HIDE:"hide"+i,HIDDEN:"hidden"+i,SHOW:"show"+i,SHOWN:"shown"+i,INSERTED:"inserted"+i,CLICK:"click"+i,FOCUSIN:"focusin"+i,FOCUSOUT:"focusout"+i,MOUSEENTER:"mouseenter"+i,MOUSELEAVE:"mouseleave"+i},g=function(r){var o,g;function p(){return r.apply(this,arguments)||this}g=r,(o=p).prototype=Object.create(g.prototype),o.prototype.constructor=o,o.__proto__=g;var m=p.prototype;return m.isWithContent=function(){return this.getTitle()||this._getContent()},m.addAttachmentClass=function(e){t(this.getTipElement()).addClass("bs-popover-"+e)},m.getTipElement=function(){return this.tip=this.tip||t(this.config.template)[0],this.tip},m.setContent=function(){var e=t(this.getTipElement());this.setElementContent(e.find(f),this.getTitle());var n=this._getContent();"function"==typeof n&&(n=n.call(this.element)),this.setElementContent(e.find(d),n),e.removeClass(c+" "+u)},m._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},m._cleanTipClass=function(){var e=t(this.getTipElement()),n=e.attr("class").match(a);null!==n&&n.length>0&&e.removeClass(n.join(""))},p._jQueryInterface=function(e){return this.each(function(){var i=t(this).data(n),s="object"==typeof e?e:null;if((i||!/destroy|hide/.test(e))&&(i||(i=new p(this,s),t(this).data(n,i)),"string"==typeof e)){if("undefined"==typeof i[e])throw new TypeError('No method named "'+e+'"');i[e]()}})},s(p,null,[{key:"VERSION",get:function(){return"4.0.0"}},{key:"Default",get:function(){return l}},{key:"NAME",get:function(){return e}},{key:"DATA_KEY",get:function(){return n}},{key:"Event",get:function(){return _}},{key:"EVENT_KEY",get:function(){return i}},{key:"DefaultType",get:function(){return h}}]),p}(U);return t.fn[e]=g._jQueryInterface,t.fn[e].Constructor=g,t.fn[e].noConflict=function(){return t.fn[e]=o,g._jQueryInterface},g}(e),K=function(t){var e="scrollspy",n="bs.scrollspy",i="."+n,o=t.fn[e],a={offset:10,method:"auto",target:""},l={offset:"number",method:"string",target:"(string|element)"},h={ACTIVATE:"activate"+i,SCROLL:"scroll"+i,LOAD_DATA_API:"load"+i+".data-api"},c="dropdown-item",u="active",f={DATA_SPY:'[data-spy="scroll"]',ACTIVE:".active",NAV_LIST_GROUP:".nav, .list-group",NAV_LINKS:".nav-link",NAV_ITEMS:".nav-item",LIST_ITEMS:".list-group-item",DROPDOWN:".dropdown",DROPDOWN_ITEMS:".dropdown-item",DROPDOWN_TOGGLE:".dropdown-toggle"},d="offset",_="position",g=function(){function o(e,n){var i=this;this._element=e,this._scrollElement="BODY"===e.tagName?window:e,this._config=this._getConfig(n),this._selector=this._config.target+" "+f.NAV_LINKS+","+this._config.target+" "+f.LIST_ITEMS+","+this._config.target+" "+f.DROPDOWN_ITEMS,this._offsets=[],this._targets=[],this._activeTarget=null,this._scrollHeight=0,t(this._scrollElement).on(h.SCROLL,function(t){return i._process(t)}),this.refresh(),this._process()}var g=o.prototype;return g.refresh=function(){var e=this,n=this._scrollElement===this._scrollElement.window?d:_,i="auto"===this._config.method?n:this._config.method,s=i===_?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),t.makeArray(t(this._selector)).map(function(e){var n,r=P.getSelectorFromElement(e);if(r&&(n=t(r)[0]),n){var o=n.getBoundingClientRect();if(o.width||o.height)return[t(n)[i]().top+s,r]}return null}).filter(function(t){return t}).sort(function(t,e){return t[0]-e[0]}).forEach(function(t){e._offsets.push(t[0]),e._targets.push(t[1])})},g.dispose=function(){t.removeData(this._element,n),t(this._scrollElement).off(i),this._element=null,this._scrollElement=null,this._config=null,this._selector=null,this._offsets=null,this._targets=null,this._activeTarget=null,this._scrollHeight=null},g._getConfig=function(n){if("string"!=typeof(n=r({},a,n)).target){var i=t(n.target).attr("id");i||(i=P.getUID(e),t(n.target).attr("id",i)),n.target="#"+i}return P.typeCheckConfig(e,n,l),n},g._getScrollTop=function(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop},g._getScrollHeight=function(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)},g._getOffsetHeight=function(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height},g._process=function(){var t=this._getScrollTop()+this._config.offset,e=this._getScrollHeight(),n=this._config.offset+e-this._getOffsetHeight();if(this._scrollHeight!==e&&this.refresh(),t>=n){var i=this._targets[this._targets.length-1];this._activeTarget!==i&&this._activate(i)}else{if(this._activeTarget&&t0)return this._activeTarget=null,void this._clear();for(var s=this._offsets.length;s--;){this._activeTarget!==this._targets[s]&&t>=this._offsets[s]&&("undefined"==typeof this._offsets[s+1]||t=4)throw new Error("Bootstrap's JavaScript requires at least jQuery v1.9.1 but less than v4.0.0")}(e),t.Util=P,t.Alert=L,t.Button=R,t.Carousel=j,t.Collapse=H,t.Dropdown=W,t.Modal=M,t.Popover=x,t.Scrollspy=K,t.Tab=V,t.Tooltip=U,Object.defineProperty(t,"__esModule",{value:!0})}); \ No newline at end of file diff --git a/public/js/lib/bowser-2.11.0.min.js b/public/js/lib/bowser-2.11.0.min.js deleted file mode 100644 index deb69d5..0000000 --- a/public/js/lib/bowser-2.11.0.min.js +++ /dev/null @@ -1 +0,0 @@ -!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.bowser=t():e.bowser=t()}(this,function(){return i={},n.m=r={17:function(e,t,r){"use strict";t.__esModule=!0,t.default=void 0;var i=r(18),r=(u.getFirstMatch=function(e,t){e=t.match(e);return e&&0a[1][s])return 1;if(a[0][s]===a[1][s]){if(s===o)return 0;--s}else if(a[0][s]"===e[0]||"<"===e[0]?(r=e.substr(1),"="===e[1]?(i=!0,r=e.substr(2)):t=[],">"===e[0]?t.push(1):t.push(-1)):"="===e[0]?r=e.substr(1):"~"===e[0]&&(i=!0,r=e.substr(1)),-1d;d++){var f=this.data.charCodeAt(d);f>65536?(b[0]=240|(1835008&f)>>>18,b[1]=128|(258048&f)>>>12,b[2]=128|(4032&f)>>>6,b[3]=128|63&f):f>2048?(b[0]=224|(61440&f)>>>12,b[1]=128|(4032&f)>>>6,b[2]=128|63&f):f>128?(b[0]=192|(1984&f)>>>6,b[1]=128|63&f):b[0]=f,this.parsedData=this.parsedData.concat(b)}this.parsedData.length!=this.data.length&&(this.parsedData.unshift(191),this.parsedData.unshift(187),this.parsedData.unshift(239))}function b(a,b){this.typeNumber=a,this.errorCorrectLevel=b,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function i(a,b){if(void 0==a.length)throw new Error(a.length+"/"+b);for(var c=0;c=f;f++){var h=0;switch(b){case d.L:h=l[f][0];break;case d.M:h=l[f][1];break;case d.Q:h=l[f][2];break;case d.H:h=l[f][3]}if(h>=e)break;c++}if(c>l.length)throw new Error("Too long data");return c}function s(a){var b=encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return b.length+(b.length!=a?3:0)}a.prototype={getLength:function(){return this.parsedData.length},write:function(a){for(var b=0,c=this.parsedData.length;c>b;b++)a.put(this.parsedData[b],8)}},b.prototype={addData:function(b){var c=new a(b);this.dataList.push(c),this.dataCache=null},isDark:function(a,b){if(0>a||this.moduleCount<=a||0>b||this.moduleCount<=b)throw new Error(a+","+b);return this.modules[a][b]},getModuleCount:function(){return this.moduleCount},make:function(){this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17,this.modules=new Array(this.moduleCount);for(var d=0;d=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,b){for(var c=-1;7>=c;c++)if(!(-1>=a+c||this.moduleCount<=a+c))for(var d=-1;7>=d;d++)-1>=b+d||this.moduleCount<=b+d||(this.modules[a+c][b+d]=c>=0&&6>=c&&(0==d||6==d)||d>=0&&6>=d&&(0==c||6==c)||c>=2&&4>=c&&d>=2&&4>=d?!0:!1)},getBestMaskPattern:function(){for(var a=0,b=0,c=0;8>c;c++){this.makeImpl(!0,c);var d=f.getLostPoint(this);(0==c||a>d)&&(a=d,b=c)}return b},createMovieClip:function(a,b,c){var d=a.createEmptyMovieClip(b,c),e=1;this.make();for(var f=0;f=g;g++)for(var h=-2;2>=h;h++)this.modules[d+g][e+h]=-2==g||2==g||-2==h||2==h||0==g&&0==h?!0:!1}},setupTypeNumber:function(a){for(var b=f.getBCHTypeNumber(this.typeNumber),c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[Math.floor(c/3)][c%3+this.moduleCount-8-3]=d}for(var c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[c%3+this.moduleCount-8-3][Math.floor(c/3)]=d}},setupTypeInfo:function(a,b){for(var c=this.errorCorrectLevel<<3|b,d=f.getBCHTypeInfo(c),e=0;15>e;e++){var g=!a&&1==(1&d>>e);6>e?this.modules[e][8]=g:8>e?this.modules[e+1][8]=g:this.modules[this.moduleCount-15+e][8]=g}for(var e=0;15>e;e++){var g=!a&&1==(1&d>>e);8>e?this.modules[8][this.moduleCount-e-1]=g:9>e?this.modules[8][15-e-1+1]=g:this.modules[8][15-e-1]=g}this.modules[this.moduleCount-8][8]=!a},mapData:function(a,b){for(var c=-1,d=this.moduleCount-1,e=7,g=0,h=this.moduleCount-1;h>0;h-=2)for(6==h&&h--;;){for(var i=0;2>i;i++)if(null==this.modules[d][h-i]){var j=!1;g>>e));var k=f.getMask(b,d,h-i);k&&(j=!j),this.modules[d][h-i]=j,e--,-1==e&&(g++,e=7)}if(d+=c,0>d||this.moduleCount<=d){d-=c,c=-c;break}}}},b.PAD0=236,b.PAD1=17,b.createData=function(a,c,d){for(var e=j.getRSBlocks(a,c),g=new k,h=0;h8*l)throw new Error("code length overflow. ("+g.getLengthInBits()+">"+8*l+")");for(g.getLengthInBits()+4<=8*l&&g.put(0,4);0!=g.getLengthInBits()%8;)g.putBit(!1);for(;;){if(g.getLengthInBits()>=8*l)break;if(g.put(b.PAD0,8),g.getLengthInBits()>=8*l)break;g.put(b.PAD1,8)}return b.createBytes(g,e)},b.createBytes=function(a,b){for(var c=0,d=0,e=0,g=new Array(b.length),h=new Array(b.length),j=0;j=0?p.get(q):0}}for(var r=0,m=0;mm;m++)for(var j=0;jm;m++)for(var j=0;j=0;)b^=f.G15<=0;)b^=f.G18<>>=1;return b},getPatternPosition:function(a){return f.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,b,c){switch(a){case e.PATTERN000:return 0==(b+c)%2;case e.PATTERN001:return 0==b%2;case e.PATTERN010:return 0==c%3;case e.PATTERN011:return 0==(b+c)%3;case e.PATTERN100:return 0==(Math.floor(b/2)+Math.floor(c/3))%2;case e.PATTERN101:return 0==b*c%2+b*c%3;case e.PATTERN110:return 0==(b*c%2+b*c%3)%2;case e.PATTERN111:return 0==(b*c%3+(b+c)%2)%2;default:throw new Error("bad maskPattern:"+a)}},getErrorCorrectPolynomial:function(a){for(var b=new i([1],0),c=0;a>c;c++)b=b.multiply(new i([1,g.gexp(c)],0));return b},getLengthInBits:function(a,b){if(b>=1&&10>b)switch(a){case c.MODE_NUMBER:return 10;case c.MODE_ALPHA_NUM:return 9;case c.MODE_8BIT_BYTE:return 8;case c.MODE_KANJI:return 8;default:throw new Error("mode:"+a)}else if(27>b)switch(a){case c.MODE_NUMBER:return 12;case c.MODE_ALPHA_NUM:return 11;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 10;default:throw new Error("mode:"+a)}else{if(!(41>b))throw new Error("type:"+b);switch(a){case c.MODE_NUMBER:return 14;case c.MODE_ALPHA_NUM:return 13;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 12;default:throw new Error("mode:"+a)}}},getLostPoint:function(a){for(var b=a.getModuleCount(),c=0,d=0;b>d;d++)for(var e=0;b>e;e++){for(var f=0,g=a.isDark(d,e),h=-1;1>=h;h++)if(!(0>d+h||d+h>=b))for(var i=-1;1>=i;i++)0>e+i||e+i>=b||(0!=h||0!=i)&&g==a.isDark(d+h,e+i)&&f++;f>5&&(c+=3+f-5)}for(var d=0;b-1>d;d++)for(var e=0;b-1>e;e++){var j=0;a.isDark(d,e)&&j++,a.isDark(d+1,e)&&j++,a.isDark(d,e+1)&&j++,a.isDark(d+1,e+1)&&j++,(0==j||4==j)&&(c+=3)}for(var d=0;b>d;d++)for(var e=0;b-6>e;e++)a.isDark(d,e)&&!a.isDark(d,e+1)&&a.isDark(d,e+2)&&a.isDark(d,e+3)&&a.isDark(d,e+4)&&!a.isDark(d,e+5)&&a.isDark(d,e+6)&&(c+=40);for(var e=0;b>e;e++)for(var d=0;b-6>d;d++)a.isDark(d,e)&&!a.isDark(d+1,e)&&a.isDark(d+2,e)&&a.isDark(d+3,e)&&a.isDark(d+4,e)&&!a.isDark(d+5,e)&&a.isDark(d+6,e)&&(c+=40);for(var k=0,e=0;b>e;e++)for(var d=0;b>d;d++)a.isDark(d,e)&&k++;var l=Math.abs(100*k/b/b-50)/5;return c+=10*l}},g={glog:function(a){if(1>a)throw new Error("glog("+a+")");return g.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;a>=256;)a-=255;return g.EXP_TABLE[a]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},h=0;8>h;h++)g.EXP_TABLE[h]=1<h;h++)g.EXP_TABLE[h]=g.EXP_TABLE[h-4]^g.EXP_TABLE[h-5]^g.EXP_TABLE[h-6]^g.EXP_TABLE[h-8];for(var h=0;255>h;h++)g.LOG_TABLE[g.EXP_TABLE[h]]=h;i.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var b=new Array(this.getLength()+a.getLength()-1),c=0;cf;f++)for(var g=c[3*f+0],h=c[3*f+1],i=c[3*f+2],k=0;g>k;k++)e.push(new j(h,i));return e},j.getRsBlockTable=function(a,b){switch(b){case d.L:return j.RS_BLOCK_TABLE[4*(a-1)+0];case d.M:return j.RS_BLOCK_TABLE[4*(a-1)+1];case d.Q:return j.RS_BLOCK_TABLE[4*(a-1)+2];case d.H:return j.RS_BLOCK_TABLE[4*(a-1)+3];default:return void 0}},k.prototype={get:function(a){var b=Math.floor(a/8);return 1==(1&this.buffer[b]>>>7-a%8)},put:function(a,b){for(var c=0;b>c;c++)this.putBit(1==(1&a>>>b-c-1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}};var l=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]],o=function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){function g(a,b){var c=document.createElementNS("http://www.w3.org/2000/svg",a);for(var d in b)b.hasOwnProperty(d)&&c.setAttribute(d,b[d]);return c}var b=this._htOption,c=this._el,d=a.getModuleCount();Math.floor(b.width/d),Math.floor(b.height/d),this.clear();var h=g("svg",{viewBox:"0 0 "+String(d)+" "+String(d),width:"100%",height:"100%",fill:b.colorLight});h.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),c.appendChild(h),h.appendChild(g("rect",{fill:b.colorDark,width:"1",height:"1",id:"template"}));for(var i=0;d>i;i++)for(var j=0;d>j;j++)if(a.isDark(i,j)){var k=g("use",{x:String(i),y:String(j)});k.setAttributeNS("http://www.w3.org/1999/xlink","href","#template"),h.appendChild(k)}},a.prototype.clear=function(){for(;this._el.hasChildNodes();)this._el.removeChild(this._el.lastChild)},a}(),p="svg"===document.documentElement.tagName.toLowerCase(),q=p?o:m()?function(){function a(){this._elImage.src=this._elCanvas.toDataURL("image/png"),this._elImage.style.display="block",this._elCanvas.style.display="none"}function d(a,b){var c=this;if(c._fFail=b,c._fSuccess=a,null===c._bSupportDataURI){var d=document.createElement("img"),e=function(){c._bSupportDataURI=!1,c._fFail&&_fFail.call(c)},f=function(){c._bSupportDataURI=!0,c._fSuccess&&c._fSuccess.call(c)};return d.onabort=e,d.onerror=e,d.onload=f,d.src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==",void 0}c._bSupportDataURI===!0&&c._fSuccess?c._fSuccess.call(c):c._bSupportDataURI===!1&&c._fFail&&c._fFail.call(c)}if(this._android&&this._android<=2.1){var b=1/window.devicePixelRatio,c=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(a,d,e,f,g,h,i,j){if("nodeName"in a&&/img/i.test(a.nodeName))for(var l=arguments.length-1;l>=1;l--)arguments[l]=arguments[l]*b;else"undefined"==typeof j&&(arguments[1]*=b,arguments[2]*=b,arguments[3]*=b,arguments[4]*=b);c.apply(this,arguments)}}var e=function(a,b){this._bIsPainted=!1,this._android=n(),this._htOption=b,this._elCanvas=document.createElement("canvas"),this._elCanvas.width=b.width,this._elCanvas.height=b.height,a.appendChild(this._elCanvas),this._el=a,this._oContext=this._elCanvas.getContext("2d"),this._bIsPainted=!1,this._elImage=document.createElement("img"),this._elImage.style.display="none",this._el.appendChild(this._elImage),this._bSupportDataURI=null};return e.prototype.draw=function(a){var b=this._elImage,c=this._oContext,d=this._htOption,e=a.getModuleCount(),f=d.width/e,g=d.height/e,h=Math.round(f),i=Math.round(g);b.style.display="none",this.clear();for(var j=0;e>j;j++)for(var k=0;e>k;k++){var l=a.isDark(j,k),m=k*f,n=j*g;c.strokeStyle=l?d.colorDark:d.colorLight,c.lineWidth=1,c.fillStyle=l?d.colorDark:d.colorLight,c.fillRect(m,n,f,g),c.strokeRect(Math.floor(m)+.5,Math.floor(n)+.5,h,i),c.strokeRect(Math.ceil(m)-.5,Math.ceil(n)-.5,h,i)}this._bIsPainted=!0},e.prototype.makeImage=function(){this._bIsPainted&&d.call(this,a)},e.prototype.isPainted=function(){return this._bIsPainted},e.prototype.clear=function(){this._oContext.clearRect(0,0,this._elCanvas.width,this._elCanvas.height),this._bIsPainted=!1},e.prototype.round=function(a){return a?Math.floor(1e3*a)/1e3:a},e}():function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){for(var b=this._htOption,c=this._el,d=a.getModuleCount(),e=Math.floor(b.width/d),f=Math.floor(b.height/d),g=[''],h=0;d>h;h++){g.push("");for(var i=0;d>i;i++)g.push('');g.push("")}g.push("
"),c.innerHTML=g.join("");var j=c.childNodes[0],k=(b.width-j.offsetWidth)/2,l=(b.height-j.offsetHeight)/2;k>0&&l>0&&(j.style.margin=l+"px "+k+"px")},a.prototype.clear=function(){this._el.innerHTML=""},a}();QRCode=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:d.H},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];"string"==typeof a&&(a=document.getElementById(a)),this._android=n(),this._el=a,this._oQRCode=null,this._oDrawing=new q(this._el,this._htOption),this._htOption.text&&this.makeCode(this._htOption.text)},QRCode.prototype.makeCode=function(a){this._oQRCode=new b(r(a,this._htOption.correctLevel),this._htOption.correctLevel),this._oQRCode.addData(a),this._oQRCode.make(),this._el.title=a,this._oDrawing.draw(this._oQRCode),this.makeImage()},QRCode.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},QRCode.prototype.clear=function(){this._oDrawing.clear()},QRCode.CorrectLevel=d}(); \ No newline at end of file diff --git a/public/js/lib/intl-tel-input-20.0.5/css/demo.css b/public/js/lib/intl-tel-input-20.0.5/css/demo.css deleted file mode 100644 index b2d9053..0000000 --- a/public/js/lib/intl-tel-input-20.0.5/css/demo.css +++ /dev/null @@ -1,51 +0,0 @@ -* { - box-sizing: border-box; -} - -body { - margin: 20px; - font-size: 14px; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - color: #555; -} - -input, -button { - height: 35px; - margin: 0; - padding: 6px 12px; - border-radius: 2px; - font-family: inherit; - font-size: 100%; - color: inherit; -} -input[disabled], -button[disabled] { - background-color: #eee; -} - -input, -select { - border: 1px solid #ccc; - width: 220px; -} - -input::placeholder { - color: #bbb; -} - -button { - color: #fff; - background-color: #428bca; - border: 1px solid #357ebd; - margin-left: 5px; -} -button:hover { - background-color: #3276b1; - border-color: #285e8e; - cursor: pointer; -} - -.notice { - margin-top: 15px; -} \ No newline at end of file diff --git a/public/js/lib/intl-tel-input-20.0.5/css/intlTelInput.css b/public/js/lib/intl-tel-input-20.0.5/css/intlTelInput.css deleted file mode 100644 index 42a9594..0000000 --- a/public/js/lib/intl-tel-input-20.0.5/css/intlTelInput.css +++ /dev/null @@ -1,1323 +0,0 @@ -.iti { - position: relative; - display: inline-block; -} -.iti * { - box-sizing: border-box; -} -.iti__hide { - display: none; -} -.iti__v-hide { - visibility: hidden; -} -.iti__a11y-text { - width: 1px; - height: 1px; - clip: rect(1px, 1px, 1px, 1px); - overflow: hidden; - position: absolute; -} -.iti input.iti__tel-input, -.iti input.iti__tel-input[type=text], -.iti input.iti__tel-input[type=tel] { - position: relative; - z-index: 0; - margin-top: 0 !important; - margin-bottom: 0 !important; - padding-right: 36px; - margin-right: 0; -} -.iti__flag-container { - position: absolute; - top: 0; - bottom: 0; - right: 0; - padding: 1px; -} -.iti__selected-flag { - z-index: 1; - position: relative; - display: flex; - align-items: center; - height: 100%; - padding: 0 6px 0 8px; -} -.iti__arrow { - margin-left: 6px; - width: 0; - height: 0; - border-left: 3px solid transparent; - border-right: 3px solid transparent; - border-top: 4px solid #555; -} -[dir=rtl] .iti__arrow { - margin-right: 6px; - margin-left: 0; -} -.iti__arrow--up { - border-top: none; - border-bottom: 4px solid #555; -} -.iti__dropdown-content { - border-radius: 3px; - background-color: white; -} -.iti--inline-dropdown .iti__dropdown-content { - position: absolute; - z-index: 2; - margin-top: 3px; - margin-left: -1px; - border: 1px solid #ccc; - box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.2); -} -.iti__dropdown-content--dropup { - bottom: 100%; - margin-bottom: 3px; -} -.iti__search-input { - width: 100%; - border-width: 0; - border-radius: 3px; - padding: 9px 12px; -} -.iti__search-input + .iti__country-list { - border-top: 1px solid #ccc; -} -.iti__country-list { - list-style: none; - padding: 0; - margin: 0; - overflow-y: scroll; - -webkit-overflow-scrolling: touch; -} -.iti--inline-dropdown .iti__country-list { - max-height: 185px; -} -.iti--flexible-dropdown-width .iti__country-list { - white-space: nowrap; -} -@media (max-width: 500px) { - .iti--flexible-dropdown-width .iti__country-list { - white-space: normal; - } -} -.iti__flag-box { - display: inline-block; - width: 20px; -} -.iti__divider { - padding-bottom: 5px; - margin-bottom: 5px; - border-bottom: 1px solid #ccc; -} -.iti__country { - display: flex; - align-items: center; - padding: 8px 8px; - outline: none; -} -.iti__dial-code { - color: #999; -} -.iti__country.iti__highlight { - background-color: rgba(0, 0, 0, 0.05); -} -.iti__flag-box, .iti__country-name { - margin-right: 8px; -} -[dir=rtl] .iti__flag-box, [dir=rtl] .iti__country-name { - margin-right: 0; - margin-left: 8px; -} -.iti--allow-dropdown input.iti__tel-input, -.iti--allow-dropdown input.iti__tel-input[type=text], -.iti--allow-dropdown input.iti__tel-input[type=tel], .iti--show-selected-dial-code input.iti__tel-input, -.iti--show-selected-dial-code input.iti__tel-input[type=text], -.iti--show-selected-dial-code input.iti__tel-input[type=tel] { - padding-right: 6px; - padding-left: 52px; - margin-left: 0; -} -[dir=rtl] .iti--allow-dropdown input.iti__tel-input, -[dir=rtl] .iti--allow-dropdown input.iti__tel-input[type=text], -[dir=rtl] .iti--allow-dropdown input.iti__tel-input[type=tel], [dir=rtl] .iti--show-selected-dial-code input.iti__tel-input, -[dir=rtl] .iti--show-selected-dial-code input.iti__tel-input[type=text], -[dir=rtl] .iti--show-selected-dial-code input.iti__tel-input[type=tel] { - padding-right: 52px; - padding-left: 6px; - margin-right: 0; -} -.iti--allow-dropdown .iti__flag-container, .iti--show-selected-dial-code .iti__flag-container { - right: auto; - left: 0; -} -[dir=rtl] .iti--allow-dropdown .iti__flag-container, [dir=rtl] .iti--show-selected-dial-code .iti__flag-container { - right: 0; - left: auto; -} -.iti--allow-dropdown .iti__flag-container:hover { - cursor: pointer; -} -.iti--allow-dropdown .iti__flag-container:hover .iti__selected-flag { - background-color: rgba(0, 0, 0, 0.05); -} -.iti--allow-dropdown .iti__flag-container:has(+ input[disabled]):hover, -.iti--allow-dropdown .iti__flag-container:has(+ input[readonly]):hover { - cursor: default; -} -.iti--allow-dropdown .iti__flag-container:has(+ input[disabled]):hover .iti__selected-flag, -.iti--allow-dropdown .iti__flag-container:has(+ input[readonly]):hover .iti__selected-flag { - background-color: transparent; -} -.iti--show-selected-dial-code .iti__selected-flag { - background-color: rgba(0, 0, 0, 0.05); -} -.iti--show-selected-dial-code.iti--show-flags .iti__selected-dial-code { - margin-left: 6px; -} -[dir=rtl] .iti--show-selected-dial-code.iti--show-flags .iti__selected-dial-code { - margin-left: 0; - margin-right: 6px; -} -.iti--container { - position: fixed; - top: -1000px; - left: -1000px; - z-index: 1060; - padding: 1px; -} -.iti--container:hover { - cursor: pointer; -} - -.iti--fullscreen-popup.iti--container { - background-color: rgba(0, 0, 0, 0.5); - top: 0; - bottom: 0; - left: 0; - right: 0; - position: fixed; - padding: 30px; - display: flex; - flex-direction: column; - justify-content: center; -} -.iti--fullscreen-popup.iti--container.iti--country-search { - justify-content: flex-start; -} -.iti--fullscreen-popup .iti__dropdown-content { - display: flex; - flex-direction: column; - max-height: 100%; - position: relative; -} -.iti--fullscreen-popup .iti__country { - padding: 10px 10px; - line-height: 1.5em; -} - -.iti__flag { - width: 20px; -} -.iti__flag.iti__be { - width: 18px; -} -.iti__flag.iti__ch { - width: 15px; -} -.iti__flag.iti__mc { - width: 19px; -} -.iti__flag.iti__ne { - width: 18px; -} -.iti__flag.iti__np { - width: 13px; -} -.iti__flag.iti__va { - width: 15px; -} -@media (min-resolution: 2x) { - .iti__flag { - background-size: 5762px 15px; - } -} -.iti__flag.iti__ac { - height: 10px; - background-position: 0px 0px; -} -.iti__flag.iti__ad { - height: 14px; - background-position: -22px 0px; -} -.iti__flag.iti__ae { - height: 10px; - background-position: -44px 0px; -} -.iti__flag.iti__af { - height: 14px; - background-position: -66px 0px; -} -.iti__flag.iti__ag { - height: 14px; - background-position: -88px 0px; -} -.iti__flag.iti__ai { - height: 10px; - background-position: -110px 0px; -} -.iti__flag.iti__al { - height: 15px; - background-position: -132px 0px; -} -.iti__flag.iti__am { - height: 10px; - background-position: -154px 0px; -} -.iti__flag.iti__ao { - height: 14px; - background-position: -176px 0px; -} -.iti__flag.iti__aq { - height: 14px; - background-position: -198px 0px; -} -.iti__flag.iti__ar { - height: 13px; - background-position: -220px 0px; -} -.iti__flag.iti__as { - height: 10px; - background-position: -242px 0px; -} -.iti__flag.iti__at { - height: 14px; - background-position: -264px 0px; -} -.iti__flag.iti__au { - height: 10px; - background-position: -286px 0px; -} -.iti__flag.iti__aw { - height: 14px; - background-position: -308px 0px; -} -.iti__flag.iti__ax { - height: 13px; - background-position: -330px 0px; -} -.iti__flag.iti__az { - height: 10px; - background-position: -352px 0px; -} -.iti__flag.iti__ba { - height: 10px; - background-position: -374px 0px; -} -.iti__flag.iti__bb { - height: 14px; - background-position: -396px 0px; -} -.iti__flag.iti__bd { - height: 12px; - background-position: -418px 0px; -} -.iti__flag.iti__be { - height: 15px; - background-position: -440px 0px; -} -.iti__flag.iti__bf { - height: 14px; - background-position: -460px 0px; -} -.iti__flag.iti__bg { - height: 12px; - background-position: -482px 0px; -} -.iti__flag.iti__bh { - height: 12px; - background-position: -504px 0px; -} -.iti__flag.iti__bi { - height: 12px; - background-position: -526px 0px; -} -.iti__flag.iti__bj { - height: 14px; - background-position: -548px 0px; -} -.iti__flag.iti__bl { - height: 14px; - background-position: -570px 0px; -} -.iti__flag.iti__bm { - height: 10px; - background-position: -592px 0px; -} -.iti__flag.iti__bn { - height: 10px; - background-position: -614px 0px; -} -.iti__flag.iti__bo { - height: 14px; - background-position: -636px 0px; -} -.iti__flag.iti__bq { - height: 14px; - background-position: -658px 0px; -} -.iti__flag.iti__br { - height: 14px; - background-position: -680px 0px; -} -.iti__flag.iti__bs { - height: 10px; - background-position: -702px 0px; -} -.iti__flag.iti__bt { - height: 14px; - background-position: -724px 0px; -} -.iti__flag.iti__bv { - height: 15px; - background-position: -746px 0px; -} -.iti__flag.iti__bw { - height: 14px; - background-position: -768px 0px; -} -.iti__flag.iti__by { - height: 10px; - background-position: -790px 0px; -} -.iti__flag.iti__bz { - height: 12px; - background-position: -812px 0px; -} -.iti__flag.iti__ca { - height: 10px; - background-position: -834px 0px; -} -.iti__flag.iti__cc { - height: 10px; - background-position: -856px 0px; -} -.iti__flag.iti__cd { - height: 15px; - background-position: -878px 0px; -} -.iti__flag.iti__cf { - height: 14px; - background-position: -900px 0px; -} -.iti__flag.iti__cg { - height: 14px; - background-position: -922px 0px; -} -.iti__flag.iti__ch { - height: 15px; - background-position: -944px 0px; -} -.iti__flag.iti__ci { - height: 14px; - background-position: -961px 0px; -} -.iti__flag.iti__ck { - height: 10px; - background-position: -983px 0px; -} -.iti__flag.iti__cl { - height: 14px; - background-position: -1005px 0px; -} -.iti__flag.iti__cm { - height: 14px; - background-position: -1027px 0px; -} -.iti__flag.iti__cn { - height: 14px; - background-position: -1049px 0px; -} -.iti__flag.iti__co { - height: 14px; - background-position: -1071px 0px; -} -.iti__flag.iti__cp { - height: 14px; - background-position: -1093px 0px; -} -.iti__flag.iti__cq { - height: 12px; - background-position: -1115px 0px; -} -.iti__flag.iti__cr { - height: 12px; - background-position: -1137px 0px; -} -.iti__flag.iti__cu { - height: 10px; - background-position: -1159px 0px; -} -.iti__flag.iti__cv { - height: 12px; - background-position: -1181px 0px; -} -.iti__flag.iti__cw { - height: 14px; - background-position: -1203px 0px; -} -.iti__flag.iti__cx { - height: 10px; - background-position: -1225px 0px; -} -.iti__flag.iti__cy { - height: 14px; - background-position: -1247px 0px; -} -.iti__flag.iti__cz { - height: 14px; - background-position: -1269px 0px; -} -.iti__flag.iti__de { - height: 12px; - background-position: -1291px 0px; -} -.iti__flag.iti__dg { - height: 10px; - background-position: -1313px 0px; -} -.iti__flag.iti__dj { - height: 14px; - background-position: -1335px 0px; -} -.iti__flag.iti__dk { - height: 15px; - background-position: -1357px 0px; -} -.iti__flag.iti__dm { - height: 10px; - background-position: -1379px 0px; -} -.iti__flag.iti__do { - height: 14px; - background-position: -1401px 0px; -} -.iti__flag.iti__dz { - height: 14px; - background-position: -1423px 0px; -} -.iti__flag.iti__ea { - height: 14px; - background-position: -1445px 0px; -} -.iti__flag.iti__ec { - height: 14px; - background-position: -1467px 0px; -} -.iti__flag.iti__ee { - height: 13px; - background-position: -1489px 0px; -} -.iti__flag.iti__eg { - height: 14px; - background-position: -1511px 0px; -} -.iti__flag.iti__eh { - height: 10px; - background-position: -1533px 0px; -} -.iti__flag.iti__er { - height: 10px; - background-position: -1555px 0px; -} -.iti__flag.iti__es { - height: 14px; - background-position: -1577px 0px; -} -.iti__flag.iti__et { - height: 10px; - background-position: -1599px 0px; -} -.iti__flag.iti__eu { - height: 14px; - background-position: -1621px 0px; -} -.iti__flag.iti__ez { - height: 14px; - background-position: -1643px 0px; -} -.iti__flag.iti__fi { - height: 12px; - background-position: -1665px 0px; -} -.iti__flag.iti__fj { - height: 10px; - background-position: -1687px 0px; -} -.iti__flag.iti__fk { - height: 10px; - background-position: -1709px 0px; -} -.iti__flag.iti__fm { - height: 11px; - background-position: -1731px 0px; -} -.iti__flag.iti__fo { - height: 15px; - background-position: -1753px 0px; -} -.iti__flag.iti__fr { - height: 14px; - background-position: -1775px 0px; -} -.iti__flag.iti__fx { - height: 14px; - background-position: -1797px 0px; -} -.iti__flag.iti__ga { - height: 15px; - background-position: -1819px 0px; -} -.iti__flag.iti__gb { - height: 10px; - background-position: -1841px 0px; -} -.iti__flag.iti__gd { - height: 12px; - background-position: -1863px 0px; -} -.iti__flag.iti__ge { - height: 14px; - background-position: -1885px 0px; -} -.iti__flag.iti__gf { - height: 14px; - background-position: -1907px 0px; -} -.iti__flag.iti__gg { - height: 14px; - background-position: -1929px 0px; -} -.iti__flag.iti__gh { - height: 14px; - background-position: -1951px 0px; -} -.iti__flag.iti__gi { - height: 10px; - background-position: -1973px 0px; -} -.iti__flag.iti__gl { - height: 14px; - background-position: -1995px 0px; -} -.iti__flag.iti__gm { - height: 14px; - background-position: -2017px 0px; -} -.iti__flag.iti__gn { - height: 14px; - background-position: -2039px 0px; -} -.iti__flag.iti__gp { - height: 14px; - background-position: -2061px 0px; -} -.iti__flag.iti__gq { - height: 14px; - background-position: -2083px 0px; -} -.iti__flag.iti__gr { - height: 14px; - background-position: -2105px 0px; -} -.iti__flag.iti__gs { - height: 10px; - background-position: -2127px 0px; -} -.iti__flag.iti__gt { - height: 13px; - background-position: -2149px 0px; -} -.iti__flag.iti__gu { - height: 11px; - background-position: -2171px 0px; -} -.iti__flag.iti__gw { - height: 10px; - background-position: -2193px 0px; -} -.iti__flag.iti__gy { - height: 12px; - background-position: -2215px 0px; -} -.iti__flag.iti__hk { - height: 14px; - background-position: -2237px 0px; -} -.iti__flag.iti__hm { - height: 10px; - background-position: -2259px 0px; -} -.iti__flag.iti__hn { - height: 10px; - background-position: -2281px 0px; -} -.iti__flag.iti__hr { - height: 10px; - background-position: -2303px 0px; -} -.iti__flag.iti__ht { - height: 12px; - background-position: -2325px 0px; -} -.iti__flag.iti__hu { - height: 10px; - background-position: -2347px 0px; -} -.iti__flag.iti__ic { - height: 14px; - background-position: -2369px 0px; -} -.iti__flag.iti__id { - height: 14px; - background-position: -2391px 0px; -} -.iti__flag.iti__ie { - height: 10px; - background-position: -2413px 0px; -} -.iti__flag.iti__il { - height: 15px; - background-position: -2435px 0px; -} -.iti__flag.iti__im { - height: 10px; - background-position: -2457px 0px; -} -.iti__flag.iti__in { - height: 14px; - background-position: -2479px 0px; -} -.iti__flag.iti__io { - height: 10px; - background-position: -2501px 0px; -} -.iti__flag.iti__iq { - height: 14px; - background-position: -2523px 0px; -} -.iti__flag.iti__ir { - height: 12px; - background-position: -2545px 0px; -} -.iti__flag.iti__is { - height: 15px; - background-position: -2567px 0px; -} -.iti__flag.iti__it { - height: 14px; - background-position: -2589px 0px; -} -.iti__flag.iti__je { - height: 12px; - background-position: -2611px 0px; -} -.iti__flag.iti__jm { - height: 10px; - background-position: -2633px 0px; -} -.iti__flag.iti__jo { - height: 10px; - background-position: -2655px 0px; -} -.iti__flag.iti__jp { - height: 14px; - background-position: -2677px 0px; -} -.iti__flag.iti__ke { - height: 14px; - background-position: -2699px 0px; -} -.iti__flag.iti__kg { - height: 12px; - background-position: -2721px 0px; -} -.iti__flag.iti__kh { - height: 13px; - background-position: -2743px 0px; -} -.iti__flag.iti__ki { - height: 10px; - background-position: -2765px 0px; -} -.iti__flag.iti__km { - height: 12px; - background-position: -2787px 0px; -} -.iti__flag.iti__kn { - height: 14px; - background-position: -2809px 0px; -} -.iti__flag.iti__kp { - height: 10px; - background-position: -2831px 0px; -} -.iti__flag.iti__kr { - height: 14px; - background-position: -2853px 0px; -} -.iti__flag.iti__kw { - height: 10px; - background-position: -2875px 0px; -} -.iti__flag.iti__ky { - height: 10px; - background-position: -2897px 0px; -} -.iti__flag.iti__kz { - height: 10px; - background-position: -2919px 0px; -} -.iti__flag.iti__la { - height: 14px; - background-position: -2941px 0px; -} -.iti__flag.iti__lb { - height: 14px; - background-position: -2963px 0px; -} -.iti__flag.iti__lc { - height: 10px; - background-position: -2985px 0px; -} -.iti__flag.iti__li { - height: 12px; - background-position: -3007px 0px; -} -.iti__flag.iti__lk { - height: 10px; - background-position: -3029px 0px; -} -.iti__flag.iti__lr { - height: 11px; - background-position: -3051px 0px; -} -.iti__flag.iti__ls { - height: 14px; - background-position: -3073px 0px; -} -.iti__flag.iti__lt { - height: 12px; - background-position: -3095px 0px; -} -.iti__flag.iti__lu { - height: 12px; - background-position: -3117px 0px; -} -.iti__flag.iti__lv { - height: 10px; - background-position: -3139px 0px; -} -.iti__flag.iti__ly { - height: 10px; - background-position: -3161px 0px; -} -.iti__flag.iti__ma { - height: 14px; - background-position: -3183px 0px; -} -.iti__flag.iti__mc { - height: 15px; - background-position: -3205px 0px; -} -.iti__flag.iti__md { - height: 10px; - background-position: -3226px 0px; -} -.iti__flag.iti__me { - height: 10px; - background-position: -3248px 0px; -} -.iti__flag.iti__mf { - height: 14px; - background-position: -3270px 0px; -} -.iti__flag.iti__mg { - height: 14px; - background-position: -3292px 0px; -} -.iti__flag.iti__mh { - height: 11px; - background-position: -3314px 0px; -} -.iti__flag.iti__mk { - height: 10px; - background-position: -3336px 0px; -} -.iti__flag.iti__ml { - height: 14px; - background-position: -3358px 0px; -} -.iti__flag.iti__mm { - height: 14px; - background-position: -3380px 0px; -} -.iti__flag.iti__mn { - height: 10px; - background-position: -3402px 0px; -} -.iti__flag.iti__mo { - height: 14px; - background-position: -3424px 0px; -} -.iti__flag.iti__mp { - height: 10px; - background-position: -3446px 0px; -} -.iti__flag.iti__mq { - height: 14px; - background-position: -3468px 0px; -} -.iti__flag.iti__mr { - height: 14px; - background-position: -3490px 0px; -} -.iti__flag.iti__ms { - height: 10px; - background-position: -3512px 0px; -} -.iti__flag.iti__mt { - height: 14px; - background-position: -3534px 0px; -} -.iti__flag.iti__mu { - height: 14px; - background-position: -3556px 0px; -} -.iti__flag.iti__mv { - height: 14px; - background-position: -3578px 0px; -} -.iti__flag.iti__mw { - height: 14px; - background-position: -3600px 0px; -} -.iti__flag.iti__mx { - height: 12px; - background-position: -3622px 0px; -} -.iti__flag.iti__my { - height: 10px; - background-position: -3644px 0px; -} -.iti__flag.iti__mz { - height: 14px; - background-position: -3666px 0px; -} -.iti__flag.iti__na { - height: 14px; - background-position: -3688px 0px; -} -.iti__flag.iti__nc { - height: 10px; - background-position: -3710px 0px; -} -.iti__flag.iti__ne { - height: 15px; - background-position: -3732px 0px; -} -.iti__flag.iti__nf { - height: 10px; - background-position: -3752px 0px; -} -.iti__flag.iti__ng { - height: 10px; - background-position: -3774px 0px; -} -.iti__flag.iti__ni { - height: 12px; - background-position: -3796px 0px; -} -.iti__flag.iti__nl { - height: 14px; - background-position: -3818px 0px; -} -.iti__flag.iti__no { - height: 15px; - background-position: -3840px 0px; -} -.iti__flag.iti__np { - height: 15px; - background-position: -3862px 0px; -} -.iti__flag.iti__nr { - height: 10px; - background-position: -3877px 0px; -} -.iti__flag.iti__nu { - height: 10px; - background-position: -3899px 0px; -} -.iti__flag.iti__nz { - height: 10px; - background-position: -3921px 0px; -} -.iti__flag.iti__om { - height: 10px; - background-position: -3943px 0px; -} -.iti__flag.iti__pa { - height: 14px; - background-position: -3965px 0px; -} -.iti__flag.iti__pe { - height: 14px; - background-position: -3987px 0px; -} -.iti__flag.iti__pf { - height: 14px; - background-position: -4009px 0px; -} -.iti__flag.iti__pg { - height: 15px; - background-position: -4031px 0px; -} -.iti__flag.iti__ph { - height: 10px; - background-position: -4053px 0px; -} -.iti__flag.iti__pk { - height: 14px; - background-position: -4075px 0px; -} -.iti__flag.iti__pl { - height: 13px; - background-position: -4097px 0px; -} -.iti__flag.iti__pm { - height: 14px; - background-position: -4119px 0px; -} -.iti__flag.iti__pn { - height: 10px; - background-position: -4141px 0px; -} -.iti__flag.iti__pr { - height: 14px; - background-position: -4163px 0px; -} -.iti__flag.iti__ps { - height: 10px; - background-position: -4185px 0px; -} -.iti__flag.iti__pt { - height: 14px; - background-position: -4207px 0px; -} -.iti__flag.iti__pw { - height: 13px; - background-position: -4229px 0px; -} -.iti__flag.iti__py { - height: 11px; - background-position: -4251px 0px; -} -.iti__flag.iti__qa { - height: 8px; - background-position: -4273px 0px; -} -.iti__flag.iti__re { - height: 14px; - background-position: -4295px 0px; -} -.iti__flag.iti__ro { - height: 14px; - background-position: -4317px 0px; -} -.iti__flag.iti__rs { - height: 14px; - background-position: -4339px 0px; -} -.iti__flag.iti__ru { - height: 14px; - background-position: -4361px 0px; -} -.iti__flag.iti__rw { - height: 14px; - background-position: -4383px 0px; -} -.iti__flag.iti__sa { - height: 14px; - background-position: -4405px 0px; -} -.iti__flag.iti__sb { - height: 10px; - background-position: -4427px 0px; -} -.iti__flag.iti__sc { - height: 10px; - background-position: -4449px 0px; -} -.iti__flag.iti__sd { - height: 10px; - background-position: -4471px 0px; -} -.iti__flag.iti__se { - height: 13px; - background-position: -4493px 0px; -} -.iti__flag.iti__sg { - height: 14px; - background-position: -4515px 0px; -} -.iti__flag.iti__sh { - height: 10px; - background-position: -4537px 0px; -} -.iti__flag.iti__si { - height: 10px; - background-position: -4559px 0px; -} -.iti__flag.iti__sj { - height: 15px; - background-position: -4581px 0px; -} -.iti__flag.iti__sk { - height: 14px; - background-position: -4603px 0px; -} -.iti__flag.iti__sl { - height: 14px; - background-position: -4625px 0px; -} -.iti__flag.iti__sm { - height: 15px; - background-position: -4647px 0px; -} -.iti__flag.iti__sn { - height: 14px; - background-position: -4669px 0px; -} -.iti__flag.iti__so { - height: 14px; - background-position: -4691px 0px; -} -.iti__flag.iti__sr { - height: 14px; - background-position: -4713px 0px; -} -.iti__flag.iti__ss { - height: 10px; - background-position: -4735px 0px; -} -.iti__flag.iti__st { - height: 10px; - background-position: -4757px 0px; -} -.iti__flag.iti__su { - height: 10px; - background-position: -4779px 0px; -} -.iti__flag.iti__sv { - height: 12px; - background-position: -4801px 0px; -} -.iti__flag.iti__sx { - height: 14px; - background-position: -4823px 0px; -} -.iti__flag.iti__sy { - height: 14px; - background-position: -4845px 0px; -} -.iti__flag.iti__sz { - height: 14px; - background-position: -4867px 0px; -} -.iti__flag.iti__ta { - height: 10px; - background-position: -4889px 0px; -} -.iti__flag.iti__tc { - height: 10px; - background-position: -4911px 0px; -} -.iti__flag.iti__td { - height: 14px; - background-position: -4933px 0px; -} -.iti__flag.iti__tf { - height: 14px; - background-position: -4955px 0px; -} -.iti__flag.iti__tg { - height: 13px; - background-position: -4977px 0px; -} -.iti__flag.iti__th { - height: 14px; - background-position: -4999px 0px; -} -.iti__flag.iti__tj { - height: 10px; - background-position: -5021px 0px; -} -.iti__flag.iti__tk { - height: 10px; - background-position: -5043px 0px; -} -.iti__flag.iti__tl { - height: 10px; - background-position: -5065px 0px; -} -.iti__flag.iti__tm { - height: 14px; - background-position: -5087px 0px; -} -.iti__flag.iti__tn { - height: 14px; - background-position: -5109px 0px; -} -.iti__flag.iti__to { - height: 10px; - background-position: -5131px 0px; -} -.iti__flag.iti__tr { - height: 14px; - background-position: -5153px 0px; -} -.iti__flag.iti__tt { - height: 12px; - background-position: -5175px 0px; -} -.iti__flag.iti__tv { - height: 10px; - background-position: -5197px 0px; -} -.iti__flag.iti__tw { - height: 14px; - background-position: -5219px 0px; -} -.iti__flag.iti__tz { - height: 14px; - background-position: -5241px 0px; -} -.iti__flag.iti__ua { - height: 14px; - background-position: -5263px 0px; -} -.iti__flag.iti__ug { - height: 14px; - background-position: -5285px 0px; -} -.iti__flag.iti__uk { - height: 10px; - background-position: -5307px 0px; -} -.iti__flag.iti__um { - height: 11px; - background-position: -5329px 0px; -} -.iti__flag.iti__un { - height: 14px; - background-position: -5351px 0px; -} -.iti__flag.iti__us { - height: 11px; - background-position: -5373px 0px; -} -.iti__flag.iti__uy { - height: 14px; - background-position: -5395px 0px; -} -.iti__flag.iti__uz { - height: 10px; - background-position: -5417px 0px; -} -.iti__flag.iti__va { - height: 15px; - background-position: -5439px 0px; -} -.iti__flag.iti__vc { - height: 14px; - background-position: -5456px 0px; -} -.iti__flag.iti__ve { - height: 14px; - background-position: -5478px 0px; -} -.iti__flag.iti__vg { - height: 10px; - background-position: -5500px 0px; -} -.iti__flag.iti__vi { - height: 14px; - background-position: -5522px 0px; -} -.iti__flag.iti__vn { - height: 14px; - background-position: -5544px 0px; -} -.iti__flag.iti__vu { - height: 12px; - background-position: -5566px 0px; -} -.iti__flag.iti__wf { - height: 14px; - background-position: -5588px 0px; -} -.iti__flag.iti__ws { - height: 10px; - background-position: -5610px 0px; -} -.iti__flag.iti__xk { - height: 15px; - background-position: -5632px 0px; -} -.iti__flag.iti__ye { - height: 14px; - background-position: -5654px 0px; -} -.iti__flag.iti__yt { - height: 14px; - background-position: -5676px 0px; -} -.iti__flag.iti__za { - height: 14px; - background-position: -5698px 0px; -} -.iti__flag.iti__zm { - height: 14px; - background-position: -5720px 0px; -} -.iti__flag.iti__zw { - height: 10px; - background-position: -5742px 0px; -} - -.iti__flag { - height: 15px; - box-shadow: 0px 0px 1px 0px #888; - background-image: url("../img/flags.png?1"); - background-repeat: no-repeat; - background-position: 20px 0; -} -@media (min-resolution: 2x) { - .iti__flag { - background-image: url("../img/flags@2x.png?1"); - } -} - -.iti__globe { - background-image: url("../img/globe.png"); - background-size: contain; - background-position: right; - box-shadow: none; - height: 19px; -} -@media (min-resolution: 2x) { - .iti__globe { - background-image: url("../img/globe@2x.png"); - } -} \ No newline at end of file diff --git a/public/js/lib/intl-tel-input-20.0.5/css/intlTelInput.min.css b/public/js/lib/intl-tel-input-20.0.5/css/intlTelInput.min.css deleted file mode 100644 index 73eff58..0000000 --- a/public/js/lib/intl-tel-input-20.0.5/css/intlTelInput.min.css +++ /dev/null @@ -1 +0,0 @@ -.iti{position:relative;display:inline-block}.iti *{box-sizing:border-box}.iti__hide{display:none}.iti__v-hide{visibility:hidden}.iti__a11y-text{width:1px;height:1px;clip:rect(1px,1px,1px,1px);overflow:hidden;position:absolute}.iti input.iti__tel-input,.iti input.iti__tel-input[type=tel],.iti input.iti__tel-input[type=text]{position:relative;z-index:0;margin-top:0!important;margin-bottom:0!important;padding-right:36px;margin-right:0}.iti__flag-container{position:absolute;top:0;bottom:0;right:0;padding:1px}.iti__selected-flag{z-index:1;position:relative;display:flex;align-items:center;height:100%;padding:0 6px 0 8px}.iti__arrow{margin-left:6px;width:0;height:0;border-left:3px solid transparent;border-right:3px solid transparent;border-top:4px solid #555}[dir=rtl] .iti__arrow{margin-right:6px;margin-left:0}.iti__arrow--up{border-top:none;border-bottom:4px solid #555}.iti__dropdown-content{border-radius:3px;background-color:#fff}.iti--inline-dropdown .iti__dropdown-content{position:absolute;z-index:2;margin-top:3px;margin-left:-1px;border:1px solid #ccc;box-shadow:1px 1px 4px rgba(0,0,0,.2)}.iti__dropdown-content--dropup{bottom:100%;margin-bottom:3px}.iti__search-input{width:100%;border-width:0;border-radius:3px;padding:9px 12px}.iti__search-input+.iti__country-list{border-top:1px solid #ccc}.iti__country-list{list-style:none;padding:0;margin:0;overflow-y:scroll;-webkit-overflow-scrolling:touch}.iti--inline-dropdown .iti__country-list{max-height:185px}.iti--flexible-dropdown-width .iti__country-list{white-space:nowrap}@media (max-width:500px){.iti--flexible-dropdown-width .iti__country-list{white-space:normal}}.iti__flag-box{display:inline-block;width:20px}.iti__divider{padding-bottom:5px;margin-bottom:5px;border-bottom:1px solid #ccc}.iti__country{display:flex;align-items:center;padding:8px 8px;outline:0}.iti__dial-code{color:#999}.iti__country.iti__highlight{background-color:rgba(0,0,0,.05)}.iti__country-name,.iti__flag-box{margin-right:8px}[dir=rtl] .iti__country-name,[dir=rtl] .iti__flag-box{margin-right:0;margin-left:8px}.iti--allow-dropdown input.iti__tel-input,.iti--allow-dropdown input.iti__tel-input[type=tel],.iti--allow-dropdown input.iti__tel-input[type=text],.iti--show-selected-dial-code input.iti__tel-input,.iti--show-selected-dial-code input.iti__tel-input[type=tel],.iti--show-selected-dial-code input.iti__tel-input[type=text]{padding-right:6px;padding-left:52px;margin-left:0}[dir=rtl] .iti--allow-dropdown input.iti__tel-input,[dir=rtl] .iti--allow-dropdown input.iti__tel-input[type=tel],[dir=rtl] .iti--allow-dropdown input.iti__tel-input[type=text],[dir=rtl] .iti--show-selected-dial-code input.iti__tel-input,[dir=rtl] .iti--show-selected-dial-code input.iti__tel-input[type=tel],[dir=rtl] .iti--show-selected-dial-code input.iti__tel-input[type=text]{padding-right:52px;padding-left:6px;margin-right:0}.iti--allow-dropdown .iti__flag-container,.iti--show-selected-dial-code .iti__flag-container{right:auto;left:0}[dir=rtl] .iti--allow-dropdown .iti__flag-container,[dir=rtl] .iti--show-selected-dial-code .iti__flag-container{right:0;left:auto}.iti--allow-dropdown .iti__flag-container:hover{cursor:pointer}.iti--allow-dropdown .iti__flag-container:hover .iti__selected-flag{background-color:rgba(0,0,0,.05)}.iti--allow-dropdown .iti__flag-container:has(+input[disabled]):hover,.iti--allow-dropdown .iti__flag-container:has(+input[readonly]):hover{cursor:default}.iti--allow-dropdown .iti__flag-container:has(+input[disabled]):hover .iti__selected-flag,.iti--allow-dropdown .iti__flag-container:has(+input[readonly]):hover .iti__selected-flag{background-color:transparent}.iti--show-selected-dial-code .iti__selected-flag{background-color:rgba(0,0,0,.05)}.iti--show-selected-dial-code.iti--show-flags .iti__selected-dial-code{margin-left:6px}[dir=rtl] .iti--show-selected-dial-code.iti--show-flags .iti__selected-dial-code{margin-left:0;margin-right:6px}.iti--container{position:fixed;top:-1000px;left:-1000px;z-index:1060;padding:1px}.iti--container:hover{cursor:pointer}.iti--fullscreen-popup.iti--container{background-color:rgba(0,0,0,.5);top:0;bottom:0;left:0;right:0;position:fixed;padding:30px;display:flex;flex-direction:column;justify-content:center}.iti--fullscreen-popup.iti--container.iti--country-search{justify-content:flex-start}.iti--fullscreen-popup .iti__dropdown-content{display:flex;flex-direction:column;max-height:100%;position:relative}.iti--fullscreen-popup .iti__country{padding:10px 10px;line-height:1.5em}.iti__flag{width:20px}.iti__flag.iti__be{width:18px}.iti__flag.iti__ch{width:15px}.iti__flag.iti__mc{width:19px}.iti__flag.iti__ne{width:18px}.iti__flag.iti__np{width:13px}.iti__flag.iti__va{width:15px}@media (min-resolution:2x){.iti__flag{background-size:5762px 15px}}.iti__flag.iti__ac{height:10px;background-position:0 0}.iti__flag.iti__ad{height:14px;background-position:-22px 0}.iti__flag.iti__ae{height:10px;background-position:-44px 0}.iti__flag.iti__af{height:14px;background-position:-66px 0}.iti__flag.iti__ag{height:14px;background-position:-88px 0}.iti__flag.iti__ai{height:10px;background-position:-110px 0}.iti__flag.iti__al{height:15px;background-position:-132px 0}.iti__flag.iti__am{height:10px;background-position:-154px 0}.iti__flag.iti__ao{height:14px;background-position:-176px 0}.iti__flag.iti__aq{height:14px;background-position:-198px 0}.iti__flag.iti__ar{height:13px;background-position:-220px 0}.iti__flag.iti__as{height:10px;background-position:-242px 0}.iti__flag.iti__at{height:14px;background-position:-264px 0}.iti__flag.iti__au{height:10px;background-position:-286px 0}.iti__flag.iti__aw{height:14px;background-position:-308px 0}.iti__flag.iti__ax{height:13px;background-position:-330px 0}.iti__flag.iti__az{height:10px;background-position:-352px 0}.iti__flag.iti__ba{height:10px;background-position:-374px 0}.iti__flag.iti__bb{height:14px;background-position:-396px 0}.iti__flag.iti__bd{height:12px;background-position:-418px 0}.iti__flag.iti__be{height:15px;background-position:-440px 0}.iti__flag.iti__bf{height:14px;background-position:-460px 0}.iti__flag.iti__bg{height:12px;background-position:-482px 0}.iti__flag.iti__bh{height:12px;background-position:-504px 0}.iti__flag.iti__bi{height:12px;background-position:-526px 0}.iti__flag.iti__bj{height:14px;background-position:-548px 0}.iti__flag.iti__bl{height:14px;background-position:-570px 0}.iti__flag.iti__bm{height:10px;background-position:-592px 0}.iti__flag.iti__bn{height:10px;background-position:-614px 0}.iti__flag.iti__bo{height:14px;background-position:-636px 0}.iti__flag.iti__bq{height:14px;background-position:-658px 0}.iti__flag.iti__br{height:14px;background-position:-680px 0}.iti__flag.iti__bs{height:10px;background-position:-702px 0}.iti__flag.iti__bt{height:14px;background-position:-724px 0}.iti__flag.iti__bv{height:15px;background-position:-746px 0}.iti__flag.iti__bw{height:14px;background-position:-768px 0}.iti__flag.iti__by{height:10px;background-position:-790px 0}.iti__flag.iti__bz{height:12px;background-position:-812px 0}.iti__flag.iti__ca{height:10px;background-position:-834px 0}.iti__flag.iti__cc{height:10px;background-position:-856px 0}.iti__flag.iti__cd{height:15px;background-position:-878px 0}.iti__flag.iti__cf{height:14px;background-position:-900px 0}.iti__flag.iti__cg{height:14px;background-position:-922px 0}.iti__flag.iti__ch{height:15px;background-position:-944px 0}.iti__flag.iti__ci{height:14px;background-position:-961px 0}.iti__flag.iti__ck{height:10px;background-position:-983px 0}.iti__flag.iti__cl{height:14px;background-position:-1005px 0}.iti__flag.iti__cm{height:14px;background-position:-1027px 0}.iti__flag.iti__cn{height:14px;background-position:-1049px 0}.iti__flag.iti__co{height:14px;background-position:-1071px 0}.iti__flag.iti__cp{height:14px;background-position:-1093px 0}.iti__flag.iti__cq{height:12px;background-position:-1115px 0}.iti__flag.iti__cr{height:12px;background-position:-1137px 0}.iti__flag.iti__cu{height:10px;background-position:-1159px 0}.iti__flag.iti__cv{height:12px;background-position:-1181px 0}.iti__flag.iti__cw{height:14px;background-position:-1203px 0}.iti__flag.iti__cx{height:10px;background-position:-1225px 0}.iti__flag.iti__cy{height:14px;background-position:-1247px 0}.iti__flag.iti__cz{height:14px;background-position:-1269px 0}.iti__flag.iti__de{height:12px;background-position:-1291px 0}.iti__flag.iti__dg{height:10px;background-position:-1313px 0}.iti__flag.iti__dj{height:14px;background-position:-1335px 0}.iti__flag.iti__dk{height:15px;background-position:-1357px 0}.iti__flag.iti__dm{height:10px;background-position:-1379px 0}.iti__flag.iti__do{height:14px;background-position:-1401px 0}.iti__flag.iti__dz{height:14px;background-position:-1423px 0}.iti__flag.iti__ea{height:14px;background-position:-1445px 0}.iti__flag.iti__ec{height:14px;background-position:-1467px 0}.iti__flag.iti__ee{height:13px;background-position:-1489px 0}.iti__flag.iti__eg{height:14px;background-position:-1511px 0}.iti__flag.iti__eh{height:10px;background-position:-1533px 0}.iti__flag.iti__er{height:10px;background-position:-1555px 0}.iti__flag.iti__es{height:14px;background-position:-1577px 0}.iti__flag.iti__et{height:10px;background-position:-1599px 0}.iti__flag.iti__eu{height:14px;background-position:-1621px 0}.iti__flag.iti__ez{height:14px;background-position:-1643px 0}.iti__flag.iti__fi{height:12px;background-position:-1665px 0}.iti__flag.iti__fj{height:10px;background-position:-1687px 0}.iti__flag.iti__fk{height:10px;background-position:-1709px 0}.iti__flag.iti__fm{height:11px;background-position:-1731px 0}.iti__flag.iti__fo{height:15px;background-position:-1753px 0}.iti__flag.iti__fr{height:14px;background-position:-1775px 0}.iti__flag.iti__fx{height:14px;background-position:-1797px 0}.iti__flag.iti__ga{height:15px;background-position:-1819px 0}.iti__flag.iti__gb{height:10px;background-position:-1841px 0}.iti__flag.iti__gd{height:12px;background-position:-1863px 0}.iti__flag.iti__ge{height:14px;background-position:-1885px 0}.iti__flag.iti__gf{height:14px;background-position:-1907px 0}.iti__flag.iti__gg{height:14px;background-position:-1929px 0}.iti__flag.iti__gh{height:14px;background-position:-1951px 0}.iti__flag.iti__gi{height:10px;background-position:-1973px 0}.iti__flag.iti__gl{height:14px;background-position:-1995px 0}.iti__flag.iti__gm{height:14px;background-position:-2017px 0}.iti__flag.iti__gn{height:14px;background-position:-2039px 0}.iti__flag.iti__gp{height:14px;background-position:-2061px 0}.iti__flag.iti__gq{height:14px;background-position:-2083px 0}.iti__flag.iti__gr{height:14px;background-position:-2105px 0}.iti__flag.iti__gs{height:10px;background-position:-2127px 0}.iti__flag.iti__gt{height:13px;background-position:-2149px 0}.iti__flag.iti__gu{height:11px;background-position:-2171px 0}.iti__flag.iti__gw{height:10px;background-position:-2193px 0}.iti__flag.iti__gy{height:12px;background-position:-2215px 0}.iti__flag.iti__hk{height:14px;background-position:-2237px 0}.iti__flag.iti__hm{height:10px;background-position:-2259px 0}.iti__flag.iti__hn{height:10px;background-position:-2281px 0}.iti__flag.iti__hr{height:10px;background-position:-2303px 0}.iti__flag.iti__ht{height:12px;background-position:-2325px 0}.iti__flag.iti__hu{height:10px;background-position:-2347px 0}.iti__flag.iti__ic{height:14px;background-position:-2369px 0}.iti__flag.iti__id{height:14px;background-position:-2391px 0}.iti__flag.iti__ie{height:10px;background-position:-2413px 0}.iti__flag.iti__il{height:15px;background-position:-2435px 0}.iti__flag.iti__im{height:10px;background-position:-2457px 0}.iti__flag.iti__in{height:14px;background-position:-2479px 0}.iti__flag.iti__io{height:10px;background-position:-2501px 0}.iti__flag.iti__iq{height:14px;background-position:-2523px 0}.iti__flag.iti__ir{height:12px;background-position:-2545px 0}.iti__flag.iti__is{height:15px;background-position:-2567px 0}.iti__flag.iti__it{height:14px;background-position:-2589px 0}.iti__flag.iti__je{height:12px;background-position:-2611px 0}.iti__flag.iti__jm{height:10px;background-position:-2633px 0}.iti__flag.iti__jo{height:10px;background-position:-2655px 0}.iti__flag.iti__jp{height:14px;background-position:-2677px 0}.iti__flag.iti__ke{height:14px;background-position:-2699px 0}.iti__flag.iti__kg{height:12px;background-position:-2721px 0}.iti__flag.iti__kh{height:13px;background-position:-2743px 0}.iti__flag.iti__ki{height:10px;background-position:-2765px 0}.iti__flag.iti__km{height:12px;background-position:-2787px 0}.iti__flag.iti__kn{height:14px;background-position:-2809px 0}.iti__flag.iti__kp{height:10px;background-position:-2831px 0}.iti__flag.iti__kr{height:14px;background-position:-2853px 0}.iti__flag.iti__kw{height:10px;background-position:-2875px 0}.iti__flag.iti__ky{height:10px;background-position:-2897px 0}.iti__flag.iti__kz{height:10px;background-position:-2919px 0}.iti__flag.iti__la{height:14px;background-position:-2941px 0}.iti__flag.iti__lb{height:14px;background-position:-2963px 0}.iti__flag.iti__lc{height:10px;background-position:-2985px 0}.iti__flag.iti__li{height:12px;background-position:-3007px 0}.iti__flag.iti__lk{height:10px;background-position:-3029px 0}.iti__flag.iti__lr{height:11px;background-position:-3051px 0}.iti__flag.iti__ls{height:14px;background-position:-3073px 0}.iti__flag.iti__lt{height:12px;background-position:-3095px 0}.iti__flag.iti__lu{height:12px;background-position:-3117px 0}.iti__flag.iti__lv{height:10px;background-position:-3139px 0}.iti__flag.iti__ly{height:10px;background-position:-3161px 0}.iti__flag.iti__ma{height:14px;background-position:-3183px 0}.iti__flag.iti__mc{height:15px;background-position:-3205px 0}.iti__flag.iti__md{height:10px;background-position:-3226px 0}.iti__flag.iti__me{height:10px;background-position:-3248px 0}.iti__flag.iti__mf{height:14px;background-position:-3270px 0}.iti__flag.iti__mg{height:14px;background-position:-3292px 0}.iti__flag.iti__mh{height:11px;background-position:-3314px 0}.iti__flag.iti__mk{height:10px;background-position:-3336px 0}.iti__flag.iti__ml{height:14px;background-position:-3358px 0}.iti__flag.iti__mm{height:14px;background-position:-3380px 0}.iti__flag.iti__mn{height:10px;background-position:-3402px 0}.iti__flag.iti__mo{height:14px;background-position:-3424px 0}.iti__flag.iti__mp{height:10px;background-position:-3446px 0}.iti__flag.iti__mq{height:14px;background-position:-3468px 0}.iti__flag.iti__mr{height:14px;background-position:-3490px 0}.iti__flag.iti__ms{height:10px;background-position:-3512px 0}.iti__flag.iti__mt{height:14px;background-position:-3534px 0}.iti__flag.iti__mu{height:14px;background-position:-3556px 0}.iti__flag.iti__mv{height:14px;background-position:-3578px 0}.iti__flag.iti__mw{height:14px;background-position:-3600px 0}.iti__flag.iti__mx{height:12px;background-position:-3622px 0}.iti__flag.iti__my{height:10px;background-position:-3644px 0}.iti__flag.iti__mz{height:14px;background-position:-3666px 0}.iti__flag.iti__na{height:14px;background-position:-3688px 0}.iti__flag.iti__nc{height:10px;background-position:-3710px 0}.iti__flag.iti__ne{height:15px;background-position:-3732px 0}.iti__flag.iti__nf{height:10px;background-position:-3752px 0}.iti__flag.iti__ng{height:10px;background-position:-3774px 0}.iti__flag.iti__ni{height:12px;background-position:-3796px 0}.iti__flag.iti__nl{height:14px;background-position:-3818px 0}.iti__flag.iti__no{height:15px;background-position:-3840px 0}.iti__flag.iti__np{height:15px;background-position:-3862px 0}.iti__flag.iti__nr{height:10px;background-position:-3877px 0}.iti__flag.iti__nu{height:10px;background-position:-3899px 0}.iti__flag.iti__nz{height:10px;background-position:-3921px 0}.iti__flag.iti__om{height:10px;background-position:-3943px 0}.iti__flag.iti__pa{height:14px;background-position:-3965px 0}.iti__flag.iti__pe{height:14px;background-position:-3987px 0}.iti__flag.iti__pf{height:14px;background-position:-4009px 0}.iti__flag.iti__pg{height:15px;background-position:-4031px 0}.iti__flag.iti__ph{height:10px;background-position:-4053px 0}.iti__flag.iti__pk{height:14px;background-position:-4075px 0}.iti__flag.iti__pl{height:13px;background-position:-4097px 0}.iti__flag.iti__pm{height:14px;background-position:-4119px 0}.iti__flag.iti__pn{height:10px;background-position:-4141px 0}.iti__flag.iti__pr{height:14px;background-position:-4163px 0}.iti__flag.iti__ps{height:10px;background-position:-4185px 0}.iti__flag.iti__pt{height:14px;background-position:-4207px 0}.iti__flag.iti__pw{height:13px;background-position:-4229px 0}.iti__flag.iti__py{height:11px;background-position:-4251px 0}.iti__flag.iti__qa{height:8px;background-position:-4273px 0}.iti__flag.iti__re{height:14px;background-position:-4295px 0}.iti__flag.iti__ro{height:14px;background-position:-4317px 0}.iti__flag.iti__rs{height:14px;background-position:-4339px 0}.iti__flag.iti__ru{height:14px;background-position:-4361px 0}.iti__flag.iti__rw{height:14px;background-position:-4383px 0}.iti__flag.iti__sa{height:14px;background-position:-4405px 0}.iti__flag.iti__sb{height:10px;background-position:-4427px 0}.iti__flag.iti__sc{height:10px;background-position:-4449px 0}.iti__flag.iti__sd{height:10px;background-position:-4471px 0}.iti__flag.iti__se{height:13px;background-position:-4493px 0}.iti__flag.iti__sg{height:14px;background-position:-4515px 0}.iti__flag.iti__sh{height:10px;background-position:-4537px 0}.iti__flag.iti__si{height:10px;background-position:-4559px 0}.iti__flag.iti__sj{height:15px;background-position:-4581px 0}.iti__flag.iti__sk{height:14px;background-position:-4603px 0}.iti__flag.iti__sl{height:14px;background-position:-4625px 0}.iti__flag.iti__sm{height:15px;background-position:-4647px 0}.iti__flag.iti__sn{height:14px;background-position:-4669px 0}.iti__flag.iti__so{height:14px;background-position:-4691px 0}.iti__flag.iti__sr{height:14px;background-position:-4713px 0}.iti__flag.iti__ss{height:10px;background-position:-4735px 0}.iti__flag.iti__st{height:10px;background-position:-4757px 0}.iti__flag.iti__su{height:10px;background-position:-4779px 0}.iti__flag.iti__sv{height:12px;background-position:-4801px 0}.iti__flag.iti__sx{height:14px;background-position:-4823px 0}.iti__flag.iti__sy{height:14px;background-position:-4845px 0}.iti__flag.iti__sz{height:14px;background-position:-4867px 0}.iti__flag.iti__ta{height:10px;background-position:-4889px 0}.iti__flag.iti__tc{height:10px;background-position:-4911px 0}.iti__flag.iti__td{height:14px;background-position:-4933px 0}.iti__flag.iti__tf{height:14px;background-position:-4955px 0}.iti__flag.iti__tg{height:13px;background-position:-4977px 0}.iti__flag.iti__th{height:14px;background-position:-4999px 0}.iti__flag.iti__tj{height:10px;background-position:-5021px 0}.iti__flag.iti__tk{height:10px;background-position:-5043px 0}.iti__flag.iti__tl{height:10px;background-position:-5065px 0}.iti__flag.iti__tm{height:14px;background-position:-5087px 0}.iti__flag.iti__tn{height:14px;background-position:-5109px 0}.iti__flag.iti__to{height:10px;background-position:-5131px 0}.iti__flag.iti__tr{height:14px;background-position:-5153px 0}.iti__flag.iti__tt{height:12px;background-position:-5175px 0}.iti__flag.iti__tv{height:10px;background-position:-5197px 0}.iti__flag.iti__tw{height:14px;background-position:-5219px 0}.iti__flag.iti__tz{height:14px;background-position:-5241px 0}.iti__flag.iti__ua{height:14px;background-position:-5263px 0}.iti__flag.iti__ug{height:14px;background-position:-5285px 0}.iti__flag.iti__uk{height:10px;background-position:-5307px 0}.iti__flag.iti__um{height:11px;background-position:-5329px 0}.iti__flag.iti__un{height:14px;background-position:-5351px 0}.iti__flag.iti__us{height:11px;background-position:-5373px 0}.iti__flag.iti__uy{height:14px;background-position:-5395px 0}.iti__flag.iti__uz{height:10px;background-position:-5417px 0}.iti__flag.iti__va{height:15px;background-position:-5439px 0}.iti__flag.iti__vc{height:14px;background-position:-5456px 0}.iti__flag.iti__ve{height:14px;background-position:-5478px 0}.iti__flag.iti__vg{height:10px;background-position:-5500px 0}.iti__flag.iti__vi{height:14px;background-position:-5522px 0}.iti__flag.iti__vn{height:14px;background-position:-5544px 0}.iti__flag.iti__vu{height:12px;background-position:-5566px 0}.iti__flag.iti__wf{height:14px;background-position:-5588px 0}.iti__flag.iti__ws{height:10px;background-position:-5610px 0}.iti__flag.iti__xk{height:15px;background-position:-5632px 0}.iti__flag.iti__ye{height:14px;background-position:-5654px 0}.iti__flag.iti__yt{height:14px;background-position:-5676px 0}.iti__flag.iti__za{height:14px;background-position:-5698px 0}.iti__flag.iti__zm{height:14px;background-position:-5720px 0}.iti__flag.iti__zw{height:10px;background-position:-5742px 0}.iti__flag{height:15px;box-shadow:0 0 1px 0 #888;background-image:url(../img/flags.png?1);background-repeat:no-repeat;background-position:20px 0}@media (min-resolution:2x){.iti__flag{background-image:url(../img/flags@2x.png?1)}}.iti__globe{background-image:url(../img/globe.png);background-size:contain;background-position:right;box-shadow:none;height:19px}@media (min-resolution:2x){.iti__globe{background-image:url(../img/globe@2x.png)}} \ No newline at end of file diff --git a/public/js/lib/intl-tel-input-20.0.5/img/flags.png b/public/js/lib/intl-tel-input-20.0.5/img/flags.png deleted file mode 100644 index 36f236e..0000000 Binary files a/public/js/lib/intl-tel-input-20.0.5/img/flags.png and /dev/null differ diff --git a/public/js/lib/intl-tel-input-20.0.5/img/flags@2x.png b/public/js/lib/intl-tel-input-20.0.5/img/flags@2x.png deleted file mode 100644 index cba08f2..0000000 Binary files a/public/js/lib/intl-tel-input-20.0.5/img/flags@2x.png and /dev/null differ diff --git a/public/js/lib/intl-tel-input-20.0.5/img/globe.png b/public/js/lib/intl-tel-input-20.0.5/img/globe.png deleted file mode 100644 index 8f380f1..0000000 Binary files a/public/js/lib/intl-tel-input-20.0.5/img/globe.png and /dev/null differ diff --git a/public/js/lib/intl-tel-input-20.0.5/img/globe@2x.png b/public/js/lib/intl-tel-input-20.0.5/img/globe@2x.png deleted file mode 100644 index 45d0db0..0000000 Binary files a/public/js/lib/intl-tel-input-20.0.5/img/globe@2x.png and /dev/null differ diff --git a/public/js/lib/intl-tel-input-20.0.5/js/data.js b/public/js/lib/intl-tel-input-20.0.5/js/data.js deleted file mode 100644 index a6b8d9d..0000000 --- a/public/js/lib/intl-tel-input-20.0.5/js/data.js +++ /dev/null @@ -1,37 +0,0 @@ -/* - * International Telephone Input v20.0.5 - * https://github.com/jackocnr/intl-tel-input.git - * Licensed under the MIT license - */ - -// wrap in UMD -(function() { - // Array of country objects for the flag dropdown. - // Here is the criteria for the plugin to support a given country/territory - // - It has an iso2 code: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 - // - It has it's own country calling code (it is not a sub-region of another country): https://en.wikipedia.org/wiki/List_of_country_calling_codes - // - It has a flag in the region-flags project: https://github.com/behdad/region-flags/tree/gh-pages/png - // - It is supported by libphonenumber (it must be listed on this page): https://github.com/googlei18n/libphonenumber/blob/master/resources/ShortNumberMetadata.xml - // Each country array has the following information: - // [ - // Country name, - // iso2 code, - // International dial code, - // Order (if >1 country with same dial code), - // Area codes - // ] - var allCountries = [ [ "Afghanistan", "af", "93" ], [ "Albania", "al", "355" ], [ "Algeria", "dz", "213" ], [ "American Samoa", "as", "1", 5, [ "684" ] ], [ "Andorra", "ad", "376" ], [ "Angola", "ao", "244" ], [ "Anguilla", "ai", "1", 6, [ "264" ] ], [ "Antigua & Barbuda", "ag", "1", 7, [ "268" ] ], [ "Argentina", "ar", "54" ], [ "Armenia", "am", "374" ], [ "Aruba", "aw", "297" ], [ "Ascension Island", "ac", "247" ], [ "Australia", "au", "61", 0 ], [ "Austria", "at", "43" ], [ "Azerbaijan", "az", "994" ], [ "Bahamas", "bs", "1", 8, [ "242" ] ], [ "Bahrain", "bh", "973" ], [ "Bangladesh", "bd", "880" ], [ "Barbados", "bb", "1", 9, [ "246" ] ], [ "Belarus", "by", "375" ], [ "Belgium", "be", "32" ], [ "Belize", "bz", "501" ], [ "Benin", "bj", "229" ], [ "Bermuda", "bm", "1", 10, [ "441" ] ], [ "Bhutan", "bt", "975" ], [ "Bolivia", "bo", "591" ], [ "Bosnia & Herzegovina", "ba", "387" ], [ "Botswana", "bw", "267" ], [ "Brazil", "br", "55" ], [ "British Indian Ocean Territory", "io", "246" ], [ "British Virgin Islands", "vg", "1", 11, [ "284" ] ], [ "Brunei", "bn", "673" ], [ "Bulgaria", "bg", "359" ], [ "Burkina Faso", "bf", "226" ], [ "Burundi", "bi", "257" ], [ "Cambodia", "kh", "855" ], [ "Cameroon", "cm", "237" ], [ "Canada", "ca", "1", 1, [ "204", "226", "236", "249", "250", "263", "289", "306", "343", "354", "365", "367", "368", "382", "387", "403", "416", "418", "428", "431", "437", "438", "450", "584", "468", "474", "506", "514", "519", "548", "579", "581", "584", "587", "604", "613", "639", "647", "672", "683", "705", "709", "742", "753", "778", "780", "782", "807", "819", "825", "867", "873", "902", "905" ] ], [ "Cape Verde", "cv", "238" ], [ "Caribbean Netherlands", "bq", "599", 1, [ "3", "4", "7" ] ], [ "Cayman Islands", "ky", "1", 12, [ "345" ] ], [ "Central African Republic", "cf", "236" ], [ "Chad", "td", "235" ], [ "Chile", "cl", "56" ], [ "China", "cn", "86" ], [ "Christmas Island", "cx", "61", 2, [ "89164" ] ], [ "Cocos (Keeling) Islands", "cc", "61", 1, [ "89162" ] ], [ "Colombia", "co", "57" ], [ "Comoros", "km", "269" ], [ "Congo - Brazzaville", "cg", "242" ], [ "Congo - Kinshasa", "cd", "243" ], [ "Cook Islands", "ck", "682" ], [ "Costa Rica", "cr", "506" ], [ "Côte d’Ivoire", "ci", "225" ], [ "Croatia", "hr", "385" ], [ "Cuba", "cu", "53" ], [ "Curaçao", "cw", "599", 0 ], [ "Cyprus", "cy", "357" ], [ "Czech Republic", "cz", "420" ], [ "Denmark", "dk", "45" ], [ "Djibouti", "dj", "253" ], [ "Dominica", "dm", "1", 13, [ "767" ] ], [ "Dominican Republic", "do", "1", 2, [ "809", "829", "849" ] ], [ "Ecuador", "ec", "593" ], [ "Egypt", "eg", "20" ], [ "El Salvador", "sv", "503" ], [ "Equatorial Guinea", "gq", "240" ], [ "Eritrea", "er", "291" ], [ "Estonia", "ee", "372" ], [ "Eswatini", "sz", "268" ], [ "Ethiopia", "et", "251" ], [ "Falkland Islands", "fk", "500" ], [ "Faroe Islands", "fo", "298" ], [ "Fiji", "fj", "679" ], [ "Finland", "fi", "358", 0 ], [ "France", "fr", "33" ], [ "French Guiana", "gf", "594" ], [ "French Polynesia", "pf", "689" ], [ "Gabon", "ga", "241" ], [ "Gambia", "gm", "220" ], [ "Georgia", "ge", "995" ], [ "Germany", "de", "49" ], [ "Ghana", "gh", "233" ], [ "Gibraltar", "gi", "350" ], [ "Greece", "gr", "30" ], [ "Greenland", "gl", "299" ], [ "Grenada", "gd", "1", 14, [ "473" ] ], [ "Guadeloupe", "gp", "590", 0 ], [ "Guam", "gu", "1", 15, [ "671" ] ], [ "Guatemala", "gt", "502" ], [ "Guernsey", "gg", "44", 1, [ "1481", "7781", "7839", "7911" ] ], [ "Guinea", "gn", "224" ], [ "Guinea-Bissau", "gw", "245" ], [ "Guyana", "gy", "592" ], [ "Haiti", "ht", "509" ], [ "Honduras", "hn", "504" ], [ "Hong Kong", "hk", "852" ], [ "Hungary", "hu", "36" ], [ "Iceland", "is", "354" ], [ "India", "in", "91" ], [ "Indonesia", "id", "62" ], [ "Iran", "ir", "98" ], [ "Iraq", "iq", "964" ], [ "Ireland", "ie", "353" ], [ "Isle of Man", "im", "44", 2, [ "1624", "74576", "7524", "7924", "7624" ] ], [ "Israel", "il", "972" ], [ "Italy", "it", "39", 0 ], [ "Jamaica", "jm", "1", 4, [ "876", "658" ] ], [ "Japan", "jp", "81" ], [ "Jersey", "je", "44", 3, [ "1534", "7509", "7700", "7797", "7829", "7937" ] ], [ "Jordan", "jo", "962" ], [ "Kazakhstan", "kz", "7", 1, [ "33", "7" ] ], [ "Kenya", "ke", "254" ], [ "Kiribati", "ki", "686" ], [ "Kosovo", "xk", "383" ], [ "Kuwait", "kw", "965" ], [ "Kyrgyzstan", "kg", "996" ], [ "Laos", "la", "856" ], [ "Latvia", "lv", "371" ], [ "Lebanon", "lb", "961" ], [ "Lesotho", "ls", "266" ], [ "Liberia", "lr", "231" ], [ "Libya", "ly", "218" ], [ "Liechtenstein", "li", "423" ], [ "Lithuania", "lt", "370" ], [ "Luxembourg", "lu", "352" ], [ "Macau", "mo", "853" ], [ "Madagascar", "mg", "261" ], [ "Malawi", "mw", "265" ], [ "Malaysia", "my", "60" ], [ "Maldives", "mv", "960" ], [ "Mali", "ml", "223" ], [ "Malta", "mt", "356" ], [ "Marshall Islands", "mh", "692" ], [ "Martinique", "mq", "596" ], [ "Mauritania", "mr", "222" ], [ "Mauritius", "mu", "230" ], [ "Mayotte", "yt", "262", 1, [ "269", "639" ] ], [ "Mexico", "mx", "52" ], [ "Micronesia", "fm", "691" ], [ "Moldova", "md", "373" ], [ "Monaco", "mc", "377" ], [ "Mongolia", "mn", "976" ], [ "Montenegro", "me", "382" ], [ "Montserrat", "ms", "1", 16, [ "664" ] ], [ "Morocco", "ma", "212", 0 ], [ "Mozambique", "mz", "258" ], [ "Myanmar (Burma)", "mm", "95" ], [ "Namibia", "na", "264" ], [ "Nauru", "nr", "674" ], [ "Nepal", "np", "977" ], [ "Netherlands", "nl", "31" ], [ "New Caledonia", "nc", "687" ], [ "New Zealand", "nz", "64" ], [ "Nicaragua", "ni", "505" ], [ "Niger", "ne", "227" ], [ "Nigeria", "ng", "234" ], [ "Niue", "nu", "683" ], [ "Norfolk Island", "nf", "672" ], [ "North Korea", "kp", "850" ], [ "North Macedonia", "mk", "389" ], [ "Northern Mariana Islands", "mp", "1", 17, [ "670" ] ], [ "Norway", "no", "47", 0 ], [ "Oman", "om", "968" ], [ "Pakistan", "pk", "92" ], [ "Palau", "pw", "680" ], [ "Palestine", "ps", "970" ], [ "Panama", "pa", "507" ], [ "Papua New Guinea", "pg", "675" ], [ "Paraguay", "py", "595" ], [ "Peru", "pe", "51" ], [ "Philippines", "ph", "63" ], [ "Poland", "pl", "48" ], [ "Portugal", "pt", "351" ], [ "Puerto Rico", "pr", "1", 3, [ "787", "939" ] ], [ "Qatar", "qa", "974" ], [ "Réunion", "re", "262", 0 ], [ "Romania", "ro", "40" ], [ "Russia", "ru", "7", 0 ], [ "Rwanda", "rw", "250" ], [ "Samoa", "ws", "685" ], [ "San Marino", "sm", "378" ], [ "São Tomé & Príncipe", "st", "239" ], [ "Saudi Arabia", "sa", "966" ], [ "Senegal", "sn", "221" ], [ "Serbia", "rs", "381" ], [ "Seychelles", "sc", "248" ], [ "Sierra Leone", "sl", "232" ], [ "Singapore", "sg", "65" ], [ "Sint Maarten", "sx", "1", 21, [ "721" ] ], [ "Slovakia", "sk", "421" ], [ "Slovenia", "si", "386" ], [ "Solomon Islands", "sb", "677" ], [ "Somalia", "so", "252" ], [ "South Africa", "za", "27" ], [ "South Korea", "kr", "82" ], [ "South Sudan", "ss", "211" ], [ "Spain", "es", "34" ], [ "Sri Lanka", "lk", "94" ], [ "St Barthélemy", "bl", "590", 1 ], [ "St Helena", "sh", "290" ], [ "St Kitts & Nevis", "kn", "1", 18, [ "869" ] ], [ "St Lucia", "lc", "1", 19, [ "758" ] ], [ "St Martin", "mf", "590", 2 ], [ "St Pierre & Miquelon", "pm", "508" ], [ "St Vincent & Grenadines", "vc", "1", 20, [ "784" ] ], [ "Sudan", "sd", "249" ], [ "Suriname", "sr", "597" ], [ "Svalbard & Jan Mayen", "sj", "47", 1, [ "79" ] ], [ "Sweden", "se", "46" ], [ "Switzerland", "ch", "41" ], [ "Syria", "sy", "963" ], [ "Taiwan", "tw", "886" ], [ "Tajikistan", "tj", "992" ], [ "Tanzania", "tz", "255" ], [ "Thailand", "th", "66" ], [ "Timor-Leste", "tl", "670" ], [ "Togo", "tg", "228" ], [ "Tokelau", "tk", "690" ], [ "Tonga", "to", "676" ], [ "Trinidad & Tobago", "tt", "1", 22, [ "868" ] ], [ "Tunisia", "tn", "216" ], [ "Turkey", "tr", "90" ], [ "Turkmenistan", "tm", "993" ], [ "Turks & Caicos Islands", "tc", "1", 23, [ "649" ] ], [ "Tuvalu", "tv", "688" ], [ "Uganda", "ug", "256" ], [ "Ukraine", "ua", "380" ], [ "United Arab Emirates", "ae", "971" ], [ "United Kingdom", "gb", "44", 0 ], [ "United States", "us", "1", 0 ], [ "Uruguay", "uy", "598" ], [ "US Virgin Islands", "vi", "1", 24, [ "340" ] ], [ "Uzbekistan", "uz", "998" ], [ "Vanuatu", "vu", "678" ], [ "Vatican City", "va", "39", 1, [ "06698" ] ], [ "Venezuela", "ve", "58" ], [ "Vietnam", "vn", "84" ], [ "Wallis & Futuna", "wf", "681" ], [ "Western Sahara", "eh", "212", 1, [ "5288", "5289" ] ], [ "Yemen", "ye", "967" ], [ "Zambia", "zm", "260" ], [ "Zimbabwe", "zw", "263" ], [ "Åland Islands", "ax", "358", 1, [ "18" ] ] ]; - // loop over all of the countries above, restructuring the data to be objects with named keys - for (var i = 0; i < allCountries.length; i++) { - var c = allCountries[i]; - allCountries[i] = { - name: c[0], - iso2: c[1], - dialCode: c[2], - priority: c[3] || 0, - areaCodes: c[4] || null, - nodeById: {} - }; - } - if (typeof module === "object" && module.exports) module.exports = allCountries; else window.allCountries = allCountries; -})(); \ No newline at end of file diff --git a/public/js/lib/intl-tel-input-20.0.5/js/data.min.js b/public/js/lib/intl-tel-input-20.0.5/js/data.min.js deleted file mode 100644 index 236ca59..0000000 --- a/public/js/lib/intl-tel-input-20.0.5/js/data.min.js +++ /dev/null @@ -1,7 +0,0 @@ -/* - * International Telephone Input v20.0.5 - * https://github.com/jackocnr/intl-tel-input.git - * Licensed under the MIT license - */ - -!function(){for(var a=[["Afghanistan","af","93"],["Albania","al","355"],["Algeria","dz","213"],["American Samoa","as","1",5,["684"]],["Andorra","ad","376"],["Angola","ao","244"],["Anguilla","ai","1",6,["264"]],["Antigua & Barbuda","ag","1",7,["268"]],["Argentina","ar","54"],["Armenia","am","374"],["Aruba","aw","297"],["Ascension Island","ac","247"],["Australia","au","61",0],["Austria","at","43"],["Azerbaijan","az","994"],["Bahamas","bs","1",8,["242"]],["Bahrain","bh","973"],["Bangladesh","bd","880"],["Barbados","bb","1",9,["246"]],["Belarus","by","375"],["Belgium","be","32"],["Belize","bz","501"],["Benin","bj","229"],["Bermuda","bm","1",10,["441"]],["Bhutan","bt","975"],["Bolivia","bo","591"],["Bosnia & Herzegovina","ba","387"],["Botswana","bw","267"],["Brazil","br","55"],["British Indian Ocean Territory","io","246"],["British Virgin Islands","vg","1",11,["284"]],["Brunei","bn","673"],["Bulgaria","bg","359"],["Burkina Faso","bf","226"],["Burundi","bi","257"],["Cambodia","kh","855"],["Cameroon","cm","237"],["Canada","ca","1",1,["204","226","236","249","250","263","289","306","343","354","365","367","368","382","387","403","416","418","428","431","437","438","450","584","468","474","506","514","519","548","579","581","584","587","604","613","639","647","672","683","705","709","742","753","778","780","782","807","819","825","867","873","902","905"]],["Cape Verde","cv","238"],["Caribbean Netherlands","bq","599",1,["3","4","7"]],["Cayman Islands","ky","1",12,["345"]],["Central African Republic","cf","236"],["Chad","td","235"],["Chile","cl","56"],["China","cn","86"],["Christmas Island","cx","61",2,["89164"]],["Cocos (Keeling) Islands","cc","61",1,["89162"]],["Colombia","co","57"],["Comoros","km","269"],["Congo - Brazzaville","cg","242"],["Congo - Kinshasa","cd","243"],["Cook Islands","ck","682"],["Costa Rica","cr","506"],["Côte d’Ivoire","ci","225"],["Croatia","hr","385"],["Cuba","cu","53"],["Curaçao","cw","599",0],["Cyprus","cy","357"],["Czech Republic","cz","420"],["Denmark","dk","45"],["Djibouti","dj","253"],["Dominica","dm","1",13,["767"]],["Dominican Republic","do","1",2,["809","829","849"]],["Ecuador","ec","593"],["Egypt","eg","20"],["El Salvador","sv","503"],["Equatorial Guinea","gq","240"],["Eritrea","er","291"],["Estonia","ee","372"],["Eswatini","sz","268"],["Ethiopia","et","251"],["Falkland Islands","fk","500"],["Faroe Islands","fo","298"],["Fiji","fj","679"],["Finland","fi","358",0],["France","fr","33"],["French Guiana","gf","594"],["French Polynesia","pf","689"],["Gabon","ga","241"],["Gambia","gm","220"],["Georgia","ge","995"],["Germany","de","49"],["Ghana","gh","233"],["Gibraltar","gi","350"],["Greece","gr","30"],["Greenland","gl","299"],["Grenada","gd","1",14,["473"]],["Guadeloupe","gp","590",0],["Guam","gu","1",15,["671"]],["Guatemala","gt","502"],["Guernsey","gg","44",1,["1481","7781","7839","7911"]],["Guinea","gn","224"],["Guinea-Bissau","gw","245"],["Guyana","gy","592"],["Haiti","ht","509"],["Honduras","hn","504"],["Hong Kong","hk","852"],["Hungary","hu","36"],["Iceland","is","354"],["India","in","91"],["Indonesia","id","62"],["Iran","ir","98"],["Iraq","iq","964"],["Ireland","ie","353"],["Isle of Man","im","44",2,["1624","74576","7524","7924","7624"]],["Israel","il","972"],["Italy","it","39",0],["Jamaica","jm","1",4,["876","658"]],["Japan","jp","81"],["Jersey","je","44",3,["1534","7509","7700","7797","7829","7937"]],["Jordan","jo","962"],["Kazakhstan","kz","7",1,["33","7"]],["Kenya","ke","254"],["Kiribati","ki","686"],["Kosovo","xk","383"],["Kuwait","kw","965"],["Kyrgyzstan","kg","996"],["Laos","la","856"],["Latvia","lv","371"],["Lebanon","lb","961"],["Lesotho","ls","266"],["Liberia","lr","231"],["Libya","ly","218"],["Liechtenstein","li","423"],["Lithuania","lt","370"],["Luxembourg","lu","352"],["Macau","mo","853"],["Madagascar","mg","261"],["Malawi","mw","265"],["Malaysia","my","60"],["Maldives","mv","960"],["Mali","ml","223"],["Malta","mt","356"],["Marshall Islands","mh","692"],["Martinique","mq","596"],["Mauritania","mr","222"],["Mauritius","mu","230"],["Mayotte","yt","262",1,["269","639"]],["Mexico","mx","52"],["Micronesia","fm","691"],["Moldova","md","373"],["Monaco","mc","377"],["Mongolia","mn","976"],["Montenegro","me","382"],["Montserrat","ms","1",16,["664"]],["Morocco","ma","212",0],["Mozambique","mz","258"],["Myanmar (Burma)","mm","95"],["Namibia","na","264"],["Nauru","nr","674"],["Nepal","np","977"],["Netherlands","nl","31"],["New Caledonia","nc","687"],["New Zealand","nz","64"],["Nicaragua","ni","505"],["Niger","ne","227"],["Nigeria","ng","234"],["Niue","nu","683"],["Norfolk Island","nf","672"],["North Korea","kp","850"],["North Macedonia","mk","389"],["Northern Mariana Islands","mp","1",17,["670"]],["Norway","no","47",0],["Oman","om","968"],["Pakistan","pk","92"],["Palau","pw","680"],["Palestine","ps","970"],["Panama","pa","507"],["Papua New Guinea","pg","675"],["Paraguay","py","595"],["Peru","pe","51"],["Philippines","ph","63"],["Poland","pl","48"],["Portugal","pt","351"],["Puerto Rico","pr","1",3,["787","939"]],["Qatar","qa","974"],["Réunion","re","262",0],["Romania","ro","40"],["Russia","ru","7",0],["Rwanda","rw","250"],["Samoa","ws","685"],["San Marino","sm","378"],["São Tomé & Príncipe","st","239"],["Saudi Arabia","sa","966"],["Senegal","sn","221"],["Serbia","rs","381"],["Seychelles","sc","248"],["Sierra Leone","sl","232"],["Singapore","sg","65"],["Sint Maarten","sx","1",21,["721"]],["Slovakia","sk","421"],["Slovenia","si","386"],["Solomon Islands","sb","677"],["Somalia","so","252"],["South Africa","za","27"],["South Korea","kr","82"],["South Sudan","ss","211"],["Spain","es","34"],["Sri Lanka","lk","94"],["St Barthélemy","bl","590",1],["St Helena","sh","290"],["St Kitts & Nevis","kn","1",18,["869"]],["St Lucia","lc","1",19,["758"]],["St Martin","mf","590",2],["St Pierre & Miquelon","pm","508"],["St Vincent & Grenadines","vc","1",20,["784"]],["Sudan","sd","249"],["Suriname","sr","597"],["Svalbard & Jan Mayen","sj","47",1,["79"]],["Sweden","se","46"],["Switzerland","ch","41"],["Syria","sy","963"],["Taiwan","tw","886"],["Tajikistan","tj","992"],["Tanzania","tz","255"],["Thailand","th","66"],["Timor-Leste","tl","670"],["Togo","tg","228"],["Tokelau","tk","690"],["Tonga","to","676"],["Trinidad & Tobago","tt","1",22,["868"]],["Tunisia","tn","216"],["Turkey","tr","90"],["Turkmenistan","tm","993"],["Turks & Caicos Islands","tc","1",23,["649"]],["Tuvalu","tv","688"],["Uganda","ug","256"],["Ukraine","ua","380"],["United Arab Emirates","ae","971"],["United Kingdom","gb","44",0],["United States","us","1",0],["Uruguay","uy","598"],["US Virgin Islands","vi","1",24,["340"]],["Uzbekistan","uz","998"],["Vanuatu","vu","678"],["Vatican City","va","39",1,["06698"]],["Venezuela","ve","58"],["Vietnam","vn","84"],["Wallis & Futuna","wf","681"],["Western Sahara","eh","212",1,["5288","5289"]],["Yemen","ye","967"],["Zambia","zm","260"],["Zimbabwe","zw","263"],["Åland Islands","ax","358",1,["18"]]],b=0;b1 country with same dial code), - // Area codes - // ] - var allCountries = [ [ "Afghanistan", "af", "93" ], [ "Albania", "al", "355" ], [ "Algeria", "dz", "213" ], [ "American Samoa", "as", "1", 5, [ "684" ] ], [ "Andorra", "ad", "376" ], [ "Angola", "ao", "244" ], [ "Anguilla", "ai", "1", 6, [ "264" ] ], [ "Antigua & Barbuda", "ag", "1", 7, [ "268" ] ], [ "Argentina", "ar", "54" ], [ "Armenia", "am", "374" ], [ "Aruba", "aw", "297" ], [ "Ascension Island", "ac", "247" ], [ "Australia", "au", "61", 0 ], [ "Austria", "at", "43" ], [ "Azerbaijan", "az", "994" ], [ "Bahamas", "bs", "1", 8, [ "242" ] ], [ "Bahrain", "bh", "973" ], [ "Bangladesh", "bd", "880" ], [ "Barbados", "bb", "1", 9, [ "246" ] ], [ "Belarus", "by", "375" ], [ "Belgium", "be", "32" ], [ "Belize", "bz", "501" ], [ "Benin", "bj", "229" ], [ "Bermuda", "bm", "1", 10, [ "441" ] ], [ "Bhutan", "bt", "975" ], [ "Bolivia", "bo", "591" ], [ "Bosnia & Herzegovina", "ba", "387" ], [ "Botswana", "bw", "267" ], [ "Brazil", "br", "55" ], [ "British Indian Ocean Territory", "io", "246" ], [ "British Virgin Islands", "vg", "1", 11, [ "284" ] ], [ "Brunei", "bn", "673" ], [ "Bulgaria", "bg", "359" ], [ "Burkina Faso", "bf", "226" ], [ "Burundi", "bi", "257" ], [ "Cambodia", "kh", "855" ], [ "Cameroon", "cm", "237" ], [ "Canada", "ca", "1", 1, [ "204", "226", "236", "249", "250", "263", "289", "306", "343", "354", "365", "367", "368", "382", "387", "403", "416", "418", "428", "431", "437", "438", "450", "584", "468", "474", "506", "514", "519", "548", "579", "581", "584", "587", "604", "613", "639", "647", "672", "683", "705", "709", "742", "753", "778", "780", "782", "807", "819", "825", "867", "873", "902", "905" ] ], [ "Cape Verde", "cv", "238" ], [ "Caribbean Netherlands", "bq", "599", 1, [ "3", "4", "7" ] ], [ "Cayman Islands", "ky", "1", 12, [ "345" ] ], [ "Central African Republic", "cf", "236" ], [ "Chad", "td", "235" ], [ "Chile", "cl", "56" ], [ "China", "cn", "86" ], [ "Christmas Island", "cx", "61", 2, [ "89164" ] ], [ "Cocos (Keeling) Islands", "cc", "61", 1, [ "89162" ] ], [ "Colombia", "co", "57" ], [ "Comoros", "km", "269" ], [ "Congo - Brazzaville", "cg", "242" ], [ "Congo - Kinshasa", "cd", "243" ], [ "Cook Islands", "ck", "682" ], [ "Costa Rica", "cr", "506" ], [ "Côte d’Ivoire", "ci", "225" ], [ "Croatia", "hr", "385" ], [ "Cuba", "cu", "53" ], [ "Curaçao", "cw", "599", 0 ], [ "Cyprus", "cy", "357" ], [ "Czech Republic", "cz", "420" ], [ "Denmark", "dk", "45" ], [ "Djibouti", "dj", "253" ], [ "Dominica", "dm", "1", 13, [ "767" ] ], [ "Dominican Republic", "do", "1", 2, [ "809", "829", "849" ] ], [ "Ecuador", "ec", "593" ], [ "Egypt", "eg", "20" ], [ "El Salvador", "sv", "503" ], [ "Equatorial Guinea", "gq", "240" ], [ "Eritrea", "er", "291" ], [ "Estonia", "ee", "372" ], [ "Eswatini", "sz", "268" ], [ "Ethiopia", "et", "251" ], [ "Falkland Islands", "fk", "500" ], [ "Faroe Islands", "fo", "298" ], [ "Fiji", "fj", "679" ], [ "Finland", "fi", "358", 0 ], [ "France", "fr", "33" ], [ "French Guiana", "gf", "594" ], [ "French Polynesia", "pf", "689" ], [ "Gabon", "ga", "241" ], [ "Gambia", "gm", "220" ], [ "Georgia", "ge", "995" ], [ "Germany", "de", "49" ], [ "Ghana", "gh", "233" ], [ "Gibraltar", "gi", "350" ], [ "Greece", "gr", "30" ], [ "Greenland", "gl", "299" ], [ "Grenada", "gd", "1", 14, [ "473" ] ], [ "Guadeloupe", "gp", "590", 0 ], [ "Guam", "gu", "1", 15, [ "671" ] ], [ "Guatemala", "gt", "502" ], [ "Guernsey", "gg", "44", 1, [ "1481", "7781", "7839", "7911" ] ], [ "Guinea", "gn", "224" ], [ "Guinea-Bissau", "gw", "245" ], [ "Guyana", "gy", "592" ], [ "Haiti", "ht", "509" ], [ "Honduras", "hn", "504" ], [ "Hong Kong", "hk", "852" ], [ "Hungary", "hu", "36" ], [ "Iceland", "is", "354" ], [ "India", "in", "91" ], [ "Indonesia", "id", "62" ], [ "Iran", "ir", "98" ], [ "Iraq", "iq", "964" ], [ "Ireland", "ie", "353" ], [ "Isle of Man", "im", "44", 2, [ "1624", "74576", "7524", "7924", "7624" ] ], [ "Israel", "il", "972" ], [ "Italy", "it", "39", 0 ], [ "Jamaica", "jm", "1", 4, [ "876", "658" ] ], [ "Japan", "jp", "81" ], [ "Jersey", "je", "44", 3, [ "1534", "7509", "7700", "7797", "7829", "7937" ] ], [ "Jordan", "jo", "962" ], [ "Kazakhstan", "kz", "7", 1, [ "33", "7" ] ], [ "Kenya", "ke", "254" ], [ "Kiribati", "ki", "686" ], [ "Kosovo", "xk", "383" ], [ "Kuwait", "kw", "965" ], [ "Kyrgyzstan", "kg", "996" ], [ "Laos", "la", "856" ], [ "Latvia", "lv", "371" ], [ "Lebanon", "lb", "961" ], [ "Lesotho", "ls", "266" ], [ "Liberia", "lr", "231" ], [ "Libya", "ly", "218" ], [ "Liechtenstein", "li", "423" ], [ "Lithuania", "lt", "370" ], [ "Luxembourg", "lu", "352" ], [ "Macau", "mo", "853" ], [ "Madagascar", "mg", "261" ], [ "Malawi", "mw", "265" ], [ "Malaysia", "my", "60" ], [ "Maldives", "mv", "960" ], [ "Mali", "ml", "223" ], [ "Malta", "mt", "356" ], [ "Marshall Islands", "mh", "692" ], [ "Martinique", "mq", "596" ], [ "Mauritania", "mr", "222" ], [ "Mauritius", "mu", "230" ], [ "Mayotte", "yt", "262", 1, [ "269", "639" ] ], [ "Mexico", "mx", "52" ], [ "Micronesia", "fm", "691" ], [ "Moldova", "md", "373" ], [ "Monaco", "mc", "377" ], [ "Mongolia", "mn", "976" ], [ "Montenegro", "me", "382" ], [ "Montserrat", "ms", "1", 16, [ "664" ] ], [ "Morocco", "ma", "212", 0 ], [ "Mozambique", "mz", "258" ], [ "Myanmar (Burma)", "mm", "95" ], [ "Namibia", "na", "264" ], [ "Nauru", "nr", "674" ], [ "Nepal", "np", "977" ], [ "Netherlands", "nl", "31" ], [ "New Caledonia", "nc", "687" ], [ "New Zealand", "nz", "64" ], [ "Nicaragua", "ni", "505" ], [ "Niger", "ne", "227" ], [ "Nigeria", "ng", "234" ], [ "Niue", "nu", "683" ], [ "Norfolk Island", "nf", "672" ], [ "North Korea", "kp", "850" ], [ "North Macedonia", "mk", "389" ], [ "Northern Mariana Islands", "mp", "1", 17, [ "670" ] ], [ "Norway", "no", "47", 0 ], [ "Oman", "om", "968" ], [ "Pakistan", "pk", "92" ], [ "Palau", "pw", "680" ], [ "Palestine", "ps", "970" ], [ "Panama", "pa", "507" ], [ "Papua New Guinea", "pg", "675" ], [ "Paraguay", "py", "595" ], [ "Peru", "pe", "51" ], [ "Philippines", "ph", "63" ], [ "Poland", "pl", "48" ], [ "Portugal", "pt", "351" ], [ "Puerto Rico", "pr", "1", 3, [ "787", "939" ] ], [ "Qatar", "qa", "974" ], [ "Réunion", "re", "262", 0 ], [ "Romania", "ro", "40" ], [ "Russia", "ru", "7", 0 ], [ "Rwanda", "rw", "250" ], [ "Samoa", "ws", "685" ], [ "San Marino", "sm", "378" ], [ "São Tomé & Príncipe", "st", "239" ], [ "Saudi Arabia", "sa", "966" ], [ "Senegal", "sn", "221" ], [ "Serbia", "rs", "381" ], [ "Seychelles", "sc", "248" ], [ "Sierra Leone", "sl", "232" ], [ "Singapore", "sg", "65" ], [ "Sint Maarten", "sx", "1", 21, [ "721" ] ], [ "Slovakia", "sk", "421" ], [ "Slovenia", "si", "386" ], [ "Solomon Islands", "sb", "677" ], [ "Somalia", "so", "252" ], [ "South Africa", "za", "27" ], [ "South Korea", "kr", "82" ], [ "South Sudan", "ss", "211" ], [ "Spain", "es", "34" ], [ "Sri Lanka", "lk", "94" ], [ "St Barthélemy", "bl", "590", 1 ], [ "St Helena", "sh", "290" ], [ "St Kitts & Nevis", "kn", "1", 18, [ "869" ] ], [ "St Lucia", "lc", "1", 19, [ "758" ] ], [ "St Martin", "mf", "590", 2 ], [ "St Pierre & Miquelon", "pm", "508" ], [ "St Vincent & Grenadines", "vc", "1", 20, [ "784" ] ], [ "Sudan", "sd", "249" ], [ "Suriname", "sr", "597" ], [ "Svalbard & Jan Mayen", "sj", "47", 1, [ "79" ] ], [ "Sweden", "se", "46" ], [ "Switzerland", "ch", "41" ], [ "Syria", "sy", "963" ], [ "Taiwan", "tw", "886" ], [ "Tajikistan", "tj", "992" ], [ "Tanzania", "tz", "255" ], [ "Thailand", "th", "66" ], [ "Timor-Leste", "tl", "670" ], [ "Togo", "tg", "228" ], [ "Tokelau", "tk", "690" ], [ "Tonga", "to", "676" ], [ "Trinidad & Tobago", "tt", "1", 22, [ "868" ] ], [ "Tunisia", "tn", "216" ], [ "Turkey", "tr", "90" ], [ "Turkmenistan", "tm", "993" ], [ "Turks & Caicos Islands", "tc", "1", 23, [ "649" ] ], [ "Tuvalu", "tv", "688" ], [ "Uganda", "ug", "256" ], [ "Ukraine", "ua", "380" ], [ "United Arab Emirates", "ae", "971" ], [ "United Kingdom", "gb", "44", 0 ], [ "United States", "us", "1", 0 ], [ "Uruguay", "uy", "598" ], [ "US Virgin Islands", "vi", "1", 24, [ "340" ] ], [ "Uzbekistan", "uz", "998" ], [ "Vanuatu", "vu", "678" ], [ "Vatican City", "va", "39", 1, [ "06698" ] ], [ "Venezuela", "ve", "58" ], [ "Vietnam", "vn", "84" ], [ "Wallis & Futuna", "wf", "681" ], [ "Western Sahara", "eh", "212", 1, [ "5288", "5289" ] ], [ "Yemen", "ye", "967" ], [ "Zambia", "zm", "260" ], [ "Zimbabwe", "zw", "263" ], [ "Åland Islands", "ax", "358", 1, [ "18" ] ] ]; - // loop over all of the countries above, restructuring the data to be objects with named keys - for (var i = 0; i < allCountries.length; i++) { - var c = allCountries[i]; - allCountries[i] = { - name: c[0], - iso2: c[1], - dialCode: c[2], - priority: c[3] || 0, - areaCodes: c[4] || null, - nodeById: {} - }; - } - "use strict"; - function ownKeys(object, enumerableOnly) { - var keys = Object.keys(object); - if (Object.getOwnPropertySymbols) { - var symbols = Object.getOwnPropertySymbols(object); - enumerableOnly && (symbols = symbols.filter(function(sym) { - return Object.getOwnPropertyDescriptor(object, sym).enumerable; - })), keys.push.apply(keys, symbols); - } - return keys; - } - function _objectSpread(target) { - for (var i = 1; i < arguments.length; i++) { - var source = null != arguments[i] ? arguments[i] : {}; - i % 2 ? ownKeys(Object(source), !0).forEach(function(key) { - _defineProperty(target, key, source[key]); - }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function(key) { - Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); - }); - } - return target; - } - function _defineProperty(obj, key, value) { - key = _toPropertyKey(key); - if (key in obj) { - Object.defineProperty(obj, key, { - value: value, - enumerable: true, - configurable: true, - writable: true - }); - } else { - obj[key] = value; - } - return obj; - } - function _slicedToArray(arr, i) { - return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); - } - function _nonIterableRest() { - throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); - } - function _unsupportedIterableToArray(o, minLen) { - if (!o) return; - if (typeof o === "string") return _arrayLikeToArray(o, minLen); - var n = Object.prototype.toString.call(o).slice(8, -1); - if (n === "Object" && o.constructor) n = o.constructor.name; - if (n === "Map" || n === "Set") return Array.from(o); - if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); - } - function _arrayLikeToArray(arr, len) { - if (len == null || len > arr.length) len = arr.length; - for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; - return arr2; - } - function _iterableToArrayLimit(arr, i) { - var _i = null == arr ? null : "undefined" != typeof Symbol && arr[Symbol.iterator] || arr["@@iterator"]; - if (null != _i) { - var _s, _e, _x, _r, _arr = [], _n = !0, _d = !1; - try { - if (_x = (_i = _i.call(arr)).next, 0 === i) { - if (Object(_i) !== _i) return; - _n = !1; - } else for (;!(_n = (_s = _x.call(_i)).done) && (_arr.push(_s.value), _arr.length !== i); _n = !0) ; - } catch (err) { - _d = !0, _e = err; - } finally { - try { - if (!_n && null != _i["return"] && (_r = _i["return"](), Object(_r) !== _r)) return; - } finally { - if (_d) throw _e; - } - } - return _arr; - } - } - function _arrayWithHoles(arr) { - if (Array.isArray(arr)) return arr; - } - function _classCallCheck(instance, Constructor) { - if (!(instance instanceof Constructor)) { - throw new TypeError("Cannot call a class as a function"); - } - } - function _defineProperties(target, props) { - for (var i = 0; i < props.length; i++) { - var descriptor = props[i]; - descriptor.enumerable = descriptor.enumerable || false; - descriptor.configurable = true; - if ("value" in descriptor) descriptor.writable = true; - Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); - } - } - function _createClass(Constructor, protoProps, staticProps) { - if (protoProps) _defineProperties(Constructor.prototype, protoProps); - if (staticProps) _defineProperties(Constructor, staticProps); - Object.defineProperty(Constructor, "prototype", { - writable: false - }); - return Constructor; - } - function _toPropertyKey(arg) { - var key = _toPrimitive(arg, "string"); - return typeof key === "symbol" ? key : String(key); - } - function _toPrimitive(input, hint) { - if (typeof input !== "object" || input === null) return input; - var prim = input[Symbol.toPrimitive]; - if (prim !== undefined) { - var res = prim.call(input, hint || "default"); - if (typeof res !== "object") return res; - throw new TypeError("@@toPrimitive must return a primitive value."); - } - return (hint === "string" ? String : Number)(input); - } - var intlTelInputGlobals = { - getInstance: function getInstance(input) { - var id = input.getAttribute("data-intl-tel-input-id"); - return window.intlTelInputGlobals.instances[id]; - }, - instances: {}, - // using a global like this allows us to mock it in the tests - documentReady: function documentReady() { - return document.readyState === "complete"; - } - }; - if (typeof window === "object") { - window.intlTelInputGlobals = intlTelInputGlobals; - } - // these vars persist through all instances of the plugin - var id = 0; - var defaults = { - // whether or not to allow the dropdown - allowDropdown: true, - // add a placeholder in the input with an example number for the selected country - autoPlaceholder: "polite", - // add a country search input at the top of the dropdown - countrySearch: true, - // modify the parentClass - containerClass: "", - // modify the auto placeholder - customPlaceholder: null, - // append menu to specified element - dropdownContainer: null, - // don't display these countries - excludeCountries: [], - // fix the dropdown width to the input width (rather than being as wide as the longest country name) - fixDropdownWidth: true, - // format the number as the user types - formatAsYouType: true, - // format the input value during initialisation and on setNumber - formatOnDisplay: true, - // geoIp lookup function - geoIpLookup: null, - // inject a hidden input with the name returned from this function, and on submit, populate it with the result of getNumber - hiddenInput: null, - // internationalise the plugin text e.g. search input placeholder, country names - i18n: {}, - // initial country - initialCountry: "", - // national vs international formatting for numbers e.g. placeholders and displaying existing numbers - nationalMode: true, - // display only these countries - onlyCountries: [], - // number type to use for placeholders - placeholderNumberType: "MOBILE", - // the countries at the top of the list - preferredCountries: [], - // option to hide the flags - must be used with showSelectedDialCode, or allowDropdown=false - showFlags: true, - // display the international dial code next to the selected flag - showSelectedDialCode: false, - // use full screen popup instead of dropdown for country list - useFullscreenPopup: typeof navigator !== "undefined" && typeof window !== "undefined" ? // we cannot just test screen size as some smartphones/website meta tags will report desktop - // resolutions - // Note: to target Android Mobiles (and not Tablets), we must find 'Android' and 'Mobile' - /Android.+Mobile|webOS|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || window.innerWidth <= 500 : false, - // specify the path to the libphonenumber script to enable validation/formatting - utilsScript: "" - }; - // https://en.wikipedia.org/wiki/List_of_North_American_Numbering_Plan_area_codes#Non-geographic_area_codes - var regionlessNanpNumbers = [ "800", "822", "833", "844", "855", "866", "877", "880", "881", "882", "883", "884", "885", "886", "887", "888", "889" ]; - // run a method on each instance of the plugin - var forEachInstance = function forEachInstance(method) { - var instances = window.intlTelInputGlobals.instances; - Object.values(instances).forEach(function(instance) { - return instance[method](); - }); - }; - // this is our plugin class that we will create an instance of - // eslint-disable-next-line no-unused-vars - var Iti = /*#__PURE__*/ function() { - function Iti(input) { - var customOptions = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; - _classCallCheck(this, Iti); - this.id = id++; - this.telInput = input; - this.activeItem = null; - this.highlightedItem = null; - // process specified options / defaults - this.options = Object.assign({}, defaults, customOptions); - this.hadInitialPlaceholder = Boolean(input.getAttribute("placeholder")); - } - _createClass(Iti, [ { - key: "_init", - value: function _init() { - var _this = this; - // if showing fullscreen popup, do not fix the width - if (this.options.useFullscreenPopup) { - this.options.fixDropdownWidth = false; - } - // when search enabled, we must fix the width else it would change with different results - if (this.options.countrySearch && !this.options.useFullscreenPopup) { - this.options.fixDropdownWidth = true; - } - // force showFlags=true if there's a dropdown and we're not displaying the dial code, - // as otherwise you just have a down arrow on it's own which doesn't make sense - var forceShowFlags = this.options.allowDropdown && !this.options.showSelectedDialCode; - if (!this.options.showFlags && forceShowFlags) { - this.options.showFlags = true; - } - // on mobile, we want a full screen dropdown, so we must append it to the body - if (this.options.useFullscreenPopup && !this.options.dropdownContainer) { - this.options.dropdownContainer = document.body; - } - // check if input has one parent with RTL - this.isRTL = !!this.telInput.closest("[dir=rtl]"); - // these promises get resolved when their individual requests complete - // this way the dev can do something like iti.promise.then(...) to know when all requests are - // complete - var autoCountryPromise = new Promise(function(resolve, reject) { - _this.resolveAutoCountryPromise = resolve; - _this.rejectAutoCountryPromise = reject; - }); - var utilsScriptPromise = new Promise(function(resolve, reject) { - _this.resolveUtilsScriptPromise = resolve; - _this.rejectUtilsScriptPromise = reject; - }); - this.promise = Promise.all([ autoCountryPromise, utilsScriptPromise ]); - // in various situations there could be no country selected initially, but we need to be able - // to assume this variable exists - this.selectedCountryData = {}; - // process all the data: onlyCountries, excludeCountries, preferredCountries etc - this._processCountryData(); - // generate the markup - this._generateMarkup(); - // set the initial state of the input value and the selected flag - this._setInitialState(); - // start all of the event listeners: input keydown, selectedFlag click - this._initListeners(); - // utils script, and auto country - this._initRequests(); - } - }, { - key: "_processCountryData", - value: function _processCountryData() { - // process onlyCountries or excludeCountries array if present - this._processAllCountries(); - // generate this.dialCodes and this.dialCodeToIso2Map - this._processDialCodes(); - // process the preferredCountries - this._processPreferredCountries(); - // translate country names according to i18n option - this._translateCountryNames(); - // sort countries by name - if (this.options.onlyCountries.length || this.options.i18n) { - this.countries.sort(this._countryNameSort); - } - } - }, { - key: "_addToDialCodeMap", - value: function _addToDialCodeMap(iso2, dialCode, priority) { - if (dialCode.length > this.dialCodeMaxLen) { - this.dialCodeMaxLen = dialCode.length; - } - if (!this.dialCodeToIso2Map.hasOwnProperty(dialCode)) { - this.dialCodeToIso2Map[dialCode] = []; - } - // bail if we already have this country for this dialCode - for (var i = 0; i < this.dialCodeToIso2Map[dialCode].length; i++) { - if (this.dialCodeToIso2Map[dialCode][i] === iso2) { - return; - } - } - // check for undefined as 0 is falsy - var index = priority !== undefined ? priority : this.dialCodeToIso2Map[dialCode].length; - this.dialCodeToIso2Map[dialCode][index] = iso2; - } - }, { - key: "_processAllCountries", - value: function _processAllCountries() { - if (this.options.onlyCountries.length) { - var lowerCaseOnlyCountries = this.options.onlyCountries.map(function(country) { - return country.toLowerCase(); - }); - this.countries = allCountries.filter(function(country) { - return lowerCaseOnlyCountries.indexOf(country.iso2) > -1; - }); - } else if (this.options.excludeCountries.length) { - var lowerCaseExcludeCountries = this.options.excludeCountries.map(function(country) { - return country.toLowerCase(); - }); - this.countries = allCountries.filter(function(country) { - return lowerCaseExcludeCountries.indexOf(country.iso2) === -1; - }); - } else { - this.countries = allCountries; - } - } - }, { - key: "_translateCountryNames", - value: function _translateCountryNames() { - for (var i = 0; i < this.countries.length; i++) { - var iso2 = this.countries[i].iso2.toLowerCase(); - if (this.options.i18n.hasOwnProperty(iso2)) { - this.countries[i].name = this.options.i18n[iso2]; - } - } - } - }, { - key: "_countryNameSort", - value: function _countryNameSort(a, b) { - if (a.name < b.name) { - return -1; - } - if (a.name > b.name) { - return 1; - } - return 0; - } - }, { - key: "_processDialCodes", - value: function _processDialCodes() { - // here we store just dial codes, where the key is the dial code, and the value is true - // e.g. { 1: true, 7: true, 20: true, ... } - this.dialCodes = {}; - this.dialCodeMaxLen = 0; - // here we map dialCodes (inc both dialCode and dialCode+areaCode) to iso2 codes - /* e.g. - * { - * 1: [ 'us', 'ca', ... ], # all NANP countries - * 12: [ 'us', 'ca', ... ], # subset of NANP countries - * 120: [ 'us', 'ca' ], # just US and Canada - * 1204: [ 'ca' ], # only Canada - * ... - * } - */ - this.dialCodeToIso2Map = {}; - // first: add dial codes - for (var i = 0; i < this.countries.length; i++) { - var c = this.countries[i]; - if (!this.dialCodes[c.dialCode]) { - this.dialCodes[c.dialCode] = true; - } - this._addToDialCodeMap(c.iso2, c.dialCode, c.priority); - } - // next: add area codes - // this is a second loop over countries, to make sure we have all of the "root" countries - // already in the map, so that we can access them, as each time we add an area code substring - // to the map, we also need to include the "root" country's code, as that also matches - for (var _i = 0; _i < this.countries.length; _i++) { - var _c = this.countries[_i]; - // area codes - if (_c.areaCodes) { - var rootIso2Code = this.dialCodeToIso2Map[_c.dialCode][0]; - // for each area code - for (var j = 0; j < _c.areaCodes.length; j++) { - var areaCode = _c.areaCodes[j]; - // for each digit in the area code to add all partial matches as well - for (var k = 1; k < areaCode.length; k++) { - var partialDialCode = _c.dialCode + areaCode.substr(0, k); - // start with the root country, as that also matches this dial code - this._addToDialCodeMap(rootIso2Code, partialDialCode); - this._addToDialCodeMap(_c.iso2, partialDialCode); - } - // add the full area code - this._addToDialCodeMap(_c.iso2, _c.dialCode + areaCode); - } - } - } - } - }, { - key: "_processPreferredCountries", - value: function _processPreferredCountries() { - this.preferredCountries = []; - for (var i = 0; i < this.options.preferredCountries.length; i++) { - var iso2 = this.options.preferredCountries[i].toLowerCase(); - var countryData = this._getCountryData(iso2, true); - if (countryData) { - this.preferredCountries.push(countryData); - } - } - } - }, { - key: "_createEl", - value: function _createEl(name, attrs, container) { - var el = document.createElement(name); - if (attrs) { - Object.entries(attrs).forEach(function(_ref) { - var _ref2 = _slicedToArray(_ref, 2), key = _ref2[0], value = _ref2[1]; - return el.setAttribute(key, value); - }); - } - if (container) { - container.appendChild(el); - } - return el; - } - }, { - key: "_generateMarkup", - value: function _generateMarkup() { - this.telInput.classList.add("iti__tel-input"); - // if autocomplete does not exist on the element and its form, then - // prevent autocomplete as there's no safe, cross-browser event we can react to, so it can - // easily put the plugin in an inconsistent state e.g. the wrong flag selected for the - // autocompleted number, which on submit could mean wrong number is saved - if (!this.telInput.hasAttribute("autocomplete") && !(this.telInput.form && this.telInput.form.hasAttribute("autocomplete"))) { - this.telInput.setAttribute("autocomplete", "off"); - } - var _this$options = this.options, allowDropdown = _this$options.allowDropdown, showSelectedDialCode = _this$options.showSelectedDialCode, showFlags = _this$options.showFlags, containerClass = _this$options.containerClass, hiddenInput = _this$options.hiddenInput, dropdownContainer = _this$options.dropdownContainer, fixDropdownWidth = _this$options.fixDropdownWidth, useFullscreenPopup = _this$options.useFullscreenPopup, countrySearch = _this$options.countrySearch, i18n = _this$options.i18n; - // containers (mostly for positioning) - var parentClass = "iti"; - if (allowDropdown) { - parentClass += " iti--allow-dropdown"; - } - if (showSelectedDialCode) { - parentClass += " iti--show-selected-dial-code"; - } - if (showFlags) { - parentClass += " iti--show-flags"; - } - if (containerClass) { - parentClass += " ".concat(containerClass); - } - if (!useFullscreenPopup) { - parentClass += " iti--inline-dropdown"; - } - var wrapper = this._createEl("div", { - "class": parentClass - }); - this.telInput.parentNode.insertBefore(wrapper, this.telInput); - // if we're showing flags or dial codes, we need the flags container etc - if (showFlags || showSelectedDialCode) { - this.flagsContainer = this._createEl("div", { - "class": "iti__flag-container" - }, wrapper); - // selected flag (displayed on left of input while allowDropdown is enabled, otherwise to right) - // when countrySearch disabled: using Aria tags for "Select-Only Combobox Example" - // https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/ - this.selectedFlag = this._createEl("div", _objectSpread({ - "class": "iti__selected-flag" - }, allowDropdown && _objectSpread({ - role: "button", - "aria-expanded": "false", - "aria-label": this.options.i18n.selectedCountryAriaLabel || "Selected country", - "aria-haspopup": countrySearch ? "true" : "listbox", - "aria-controls": countrySearch ? "iti-".concat(this.id, "__dropdown-content") : "iti-".concat(this.id, "__country-listbox") - }, countrySearch || { - role: "combobox" - })), this.flagsContainer); - // we now include the selected flag element even when showFlags is disabled, - // as need to show globe icon for showSelectedDialCode empty state - this.selectedFlagInner = this._createEl("div", null, this.selectedFlag); - this.selectedFlagA11yText = this._createEl("span", { - "class": "iti__a11y-text" - }, this.selectedFlagInner); - } - wrapper.appendChild(this.telInput); - if (this.selectedFlag && this.telInput.disabled) { - this.selectedFlag.setAttribute("aria-disabled", "true"); - } - if (showSelectedDialCode) { - this.selectedDialCode = this._createEl("div", { - "class": "iti__selected-dial-code" - }, this.selectedFlag); - } - if (allowDropdown) { - if (!this.telInput.disabled) { - // make element focusable and tab navigable - this.selectedFlag.setAttribute("tabindex", "0"); - } - this.dropdownArrow = this._createEl("div", { - "class": "iti__arrow", - "aria-hidden": "true" - }, this.selectedFlag); - var extraClasses = fixDropdownWidth ? "" : "iti--flexible-dropdown-width"; - this.dropdownContent = this._createEl("div", { - id: "iti-".concat(this.id, "__dropdown-content"), - "class": "iti__dropdown-content iti__hide ".concat(extraClasses) - }); - if (countrySearch) { - this.searchInput = this._createEl("input", { - type: "text", - "class": "iti__search-input", - placeholder: i18n.searchPlaceholder || "Search", - role: "combobox", - "aria-expanded": "true", - "aria-label": i18n.searchPlaceholder || "Search", - "aria-controls": "iti-".concat(this.id, "__country-listbox"), - "aria-autocomplete": "list", - autocomplete: "off" - }, this.dropdownContent); - this.searchResultsA11yText = this._createEl("span", { - "class": "iti__a11y-text" - }, this.dropdownContent); - } - // country list: preferred countries, then divider, then all countries - this.countryList = this._createEl("ul", { - "class": "iti__country-list", - id: "iti-".concat(this.id, "__country-listbox"), - role: "listbox", - "aria-label": i18n.countryListAriaLabel || "List of countries" - }, this.dropdownContent); - if (this.preferredCountries.length && !countrySearch) { - this._appendListItems(this.preferredCountries, "iti__preferred", true); - this._createEl("li", { - "class": "iti__divider", - "aria-hidden": "true" - }, this.countryList); - } - this._appendListItems(this.countries, "iti__standard"); - if (countrySearch) { - this._updateSearchResultsText(); - } - // create dropdownContainer markup - if (dropdownContainer) { - var dropdownClasses = "iti iti--container"; - if (useFullscreenPopup) { - dropdownClasses += " iti--fullscreen-popup"; - } else { - dropdownClasses += " iti--inline-dropdown"; - } - if (countrySearch) { - dropdownClasses += " iti--country-search"; - } - this.dropdown = this._createEl("div", { - "class": dropdownClasses - }); - this.dropdown.appendChild(this.dropdownContent); - } else { - this.flagsContainer.appendChild(this.dropdownContent); - } - } - if (hiddenInput) { - var telInputName = this.telInput.getAttribute("name"); - var names = hiddenInput(telInputName); - if (names.phone) { - // Create hidden input for the full international number - this.hiddenInput = this._createEl("input", { - type: "hidden", - name: names.phone - }); - wrapper.appendChild(this.hiddenInput); - } - if (names.country) { - // Create hidden input for the selected country iso2 code - this.hiddenInputCountry = this._createEl("input", { - type: "hidden", - name: names.country - }); - wrapper.appendChild(this.hiddenInputCountry); - } - } - } - }, { - key: "_appendListItems", - value: function _appendListItems(countries, className, preferred) { - for (var i = 0; i < countries.length; i++) { - var c = countries[i]; - var idSuffix = preferred ? "-preferred" : ""; - var listItem = this._createEl("li", { - id: "iti-".concat(this.id, "__item-").concat(c.iso2).concat(idSuffix), - "class": "iti__country ".concat(className), - tabindex: "-1", - role: "option", - "data-dial-code": c.dialCode, - "data-country-code": c.iso2, - "aria-selected": "false" - }, this.countryList); - // store this for later use e.g. country search filtering - c.nodeById[this.id] = listItem; - var content = ""; - // add the flag - if (this.options.showFlags) { - content += "
"); - } - // and the country name and dial code - content += "".concat(c.name, ""); - content += "+".concat(c.dialCode, ""); - listItem.insertAdjacentHTML("beforeend", content); - } - } - }, { - key: "_setInitialState", - value: function _setInitialState() { - var overrideAutoCountry = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; - // fix firefox bug: when first load page (with input with value set to number with intl dial - // code) and initialising plugin removes the dial code from the input, then refresh page, - // and we try to init plugin again but this time on number without dial code so get grey flag - var attributeValue = this.telInput.getAttribute("value"); - var inputValue = this.telInput.value; - var useAttribute = attributeValue && attributeValue.charAt(0) === "+" && (!inputValue || inputValue.charAt(0) !== "+"); - var val = useAttribute ? attributeValue : inputValue; - var dialCode = this._getDialCode(val); - var isRegionlessNanp = this._isRegionlessNanp(val); - var initialCountry = this.options.initialCountry; - // if we already have a dial code, and it's not a regionlessNanp, we can go ahead and set the - // flag, else fall back to the default country - if (dialCode && !isRegionlessNanp) { - this._updateFlagFromNumber(val); - } else if (initialCountry !== "auto" || overrideAutoCountry) { - var lowerInitialCountry = initialCountry ? initialCountry.toLowerCase() : ""; - var isValidInitialCountry = lowerInitialCountry && this._getCountryData(lowerInitialCountry, true); - // see if we should select a flag - if (isValidInitialCountry) { - this._setCountry(lowerInitialCountry); - } else { - if (dialCode && isRegionlessNanp) { - // has intl dial code, is regionless nanp, and no initialCountry, so default to US - this._setCountry("us"); - } else { - // display the empty state (globe icon) - this._setCountry(); - } - } - } - // NOTE: if initialCountry is set to auto, that will be handled separately - // format - note this wont be run after _updateDialCode as that's only called if no val - if (val) { - this._updateValFromNumber(val); - } - } - }, { - key: "_initListeners", - value: function _initListeners() { - this._initKeyListeners(); - if (this.options.allowDropdown) { - this._initDropdownListeners(); - } - if ((this.hiddenInput || this.hiddenInputCountry) && this.telInput.form) { - this._initHiddenInputListener(); - } - } - }, { - key: "_initHiddenInputListener", - value: function _initHiddenInputListener() { - var _this2 = this; - this._handleHiddenInputSubmit = function() { - if (_this2.hiddenInput) { - _this2.hiddenInput.value = _this2.getNumber(); - } - if (_this2.hiddenInputCountry) { - _this2.hiddenInputCountry.value = _this2.getSelectedCountryData().iso2; - } - }; - this.telInput.form.addEventListener("submit", this._handleHiddenInputSubmit); - } - }, { - key: "_initDropdownListeners", - value: function _initDropdownListeners() { - var _this3 = this; - // hack for input nested inside label (which is valid markup): clicking the selected-flag to - // open the dropdown would then automatically trigger a 2nd click on the input which would - // close it again - this._handleLabelClick = function(e) { - // if the dropdown is closed, then focus the input, else ignore the click - if (_this3.dropdownContent.classList.contains("iti__hide")) { - _this3.telInput.focus(); - } else { - e.preventDefault(); - } - }; - var label = this.telInput.closest("label"); - if (label) { - label.addEventListener("click", this._handleLabelClick); - } - // toggle country dropdown on click - this._handleClickSelectedFlag = function() { - // only intercept this event if we're opening the dropdown - // else let it bubble up to the top ("click-off-to-close" listener) - // we cannot just stopPropagation as it may be needed to close another instance - if (_this3.dropdownContent.classList.contains("iti__hide") && !_this3.telInput.disabled && !_this3.telInput.readOnly) { - _this3._openDropdown(); - } - }; - this.selectedFlag.addEventListener("click", this._handleClickSelectedFlag); - // open dropdown if selected flag is focused and they press up/down/space/enter - this._handleFlagsContainerKeydown = function(e) { - var isDropdownHidden = _this3.dropdownContent.classList.contains("iti__hide"); - if (isDropdownHidden && [ "ArrowUp", "ArrowDown", " ", "Enter" ].includes(e.key)) { - // prevent form from being submitted if "ENTER" was pressed - e.preventDefault(); - // prevent event from being handled again by document - e.stopPropagation(); - _this3._openDropdown(); - } - // allow navigation from dropdown to input on TAB - if (e.key === "Tab") { - _this3._closeDropdown(); - } - }; - this.flagsContainer.addEventListener("keydown", this._handleFlagsContainerKeydown); - } - }, { - key: "_initRequests", - value: function _initRequests() { - var _this4 = this; - // if the user has specified the path to the utils script, fetch it on window.load, else resolve - if (this.options.utilsScript && !window.intlTelInputUtils) { - // if the plugin is being initialised after the window.load event has already been fired - if (window.intlTelInputGlobals.documentReady()) { - window.intlTelInputGlobals.loadUtils(this.options.utilsScript); - } else { - // wait until the load event so we don't block any other requests e.g. the flags image - window.addEventListener("load", function() { - window.intlTelInputGlobals.loadUtils(_this4.options.utilsScript); - }); - } - } else { - this.resolveUtilsScriptPromise(); - } - // dont bother with IP lookup if we already have a selected country - if (this.options.initialCountry === "auto" && !this.selectedCountryData.iso2) { - this._loadAutoCountry(); - } else { - this.resolveAutoCountryPromise(); - } - } - }, { - key: "_loadAutoCountry", - value: function _loadAutoCountry() { - var _this5 = this; - // 3 options: - // 1) already loaded (we're done) - // 2) not already started loading (start) - // 3) already started loading (do nothing - just wait for loading callback to fire) - if (window.intlTelInputGlobals.autoCountry) { - this.handleAutoCountry(); - } else if (!window.intlTelInputGlobals.startedLoadingAutoCountry) { - // don't do this twice! - window.intlTelInputGlobals.startedLoadingAutoCountry = true; - if (typeof this.options.geoIpLookup === "function") { - this.options.geoIpLookup(function() { - var iso2 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ""; - var iso2Lower = iso2.toLowerCase(); - var isValidIso2 = iso2Lower && _this5._getCountryData(iso2Lower, true); - if (isValidIso2) { - window.intlTelInputGlobals.autoCountry = iso2Lower; - // tell all instances the auto country is ready - // TODO: this should just be the current instances - // UPDATE: use setTimeout in case their geoIpLookup function calls this callback straight - // away (e.g. if they have already done the geo ip lookup somewhere else). Using - // setTimeout means that the current thread of execution will finish before executing - // this, which allows the plugin to finish initialising. - setTimeout(function() { - return forEachInstance("handleAutoCountry"); - }); - } else { - _this5._setInitialState(true); - forEachInstance("rejectAutoCountryPromise"); - } - }, function() { - return forEachInstance("rejectAutoCountryPromise"); - }); - } - } - } - }, { - key: "_initKeyListeners", - value: function _initKeyListeners() { - var _this6 = this; - var userOverrideFormatting = false; - // update flag on input event - this._handleKeyEvent = function(e) { - if (_this6._updateFlagFromNumber(_this6.telInput.value)) { - _this6._triggerCountryChange(); - } - // if user types their own formatting char (not a plus or a numeric), then set the override - var isFormattingChar = e && e.data && /[^+0-9]/.test(e.data); - var isPaste = e && e.inputType === "insertFromPaste" && _this6.telInput.value; - if (isFormattingChar || isPaste) { - userOverrideFormatting = true; - } else if (!/[^+0-9]/.test(_this6.telInput.value)) { - userOverrideFormatting = false; - } - // handle FAYT, unless userOverrideFormatting or it's a paste event - if (_this6.options.formatAsYouType && !userOverrideFormatting && !isPaste) { - // maintain caret position after reformatting - var currentCaretPos = _this6.telInput.selectionStart; - var valueBeforeCaret = _this6.telInput.value.substring(0, currentCaretPos); - var relevantCharsBeforeCaret = valueBeforeCaret.replace(/[^+0-9]/g, "").length; - var isDeleteForwards = e && e.inputType === "deleteContentForward"; - var formattedValue = _this6._formatNumberAsYouType(); - var newCaretPos = _this6._translateCursorPosition(relevantCharsBeforeCaret, formattedValue, currentCaretPos, isDeleteForwards); - _this6.telInput.value = formattedValue; - _this6.telInput.setSelectionRange(newCaretPos, newCaretPos); - } - }; - // this handles individual key presses as well as cut/paste events - // the advantage of the "input" event over "keyup" etc is that "input" only fires when the value changes, - // whereas "keyup" fires even for arrow key presses etc - this.telInput.addEventListener("input", this._handleKeyEvent); - } - }, { - key: "_translateCursorPosition", - value: function _translateCursorPosition(relevantChars, formattedValue, prevCaretPos, isDeleteForwards) { - // if the first char is a formatting char, and they backspace delete it: - // cursor should stay at the start (pos 0), rather than stick to the first digit (pos 1) - if (prevCaretPos === 0 && !isDeleteForwards) { - return 0; - } - var count = 0; - for (var i = 0; i < formattedValue.length; i++) { - if (/[+0-9]/.test(formattedValue[i])) { - count++; - } - // normal case: stop when you hit the right number of relevant chars - // (cursor will be just after the final relevant char) - if (count === relevantChars && !isDeleteForwards) { - return i + 1; - } - // spacial case: delete forwards (fn + delete on a mac): - // wait until hit one extra relevant char, and put the cursor just before it (after any formatting chars) - if (isDeleteForwards && count === relevantChars + 1) { - return i; - } - } - return formattedValue.length; - } - }, { - key: "_cap", - value: function _cap(number) { - var max = this.telInput.getAttribute("maxlength"); - return max && number.length > max ? number.substr(0, max) : number; - } - }, { - key: "_removeEmptyDialCode", - value: function _removeEmptyDialCode() { - if (this.telInput.value.charAt(0) === "+") { - var numeric = this._getNumeric(this.telInput.value); - // if just a plus, or if just a dial code - if (!numeric || this.selectedCountryData.dialCode === numeric) { - this.telInput.value = ""; - } - } - } - }, { - key: "_getNumeric", - value: function _getNumeric(s) { - return s.replace(/\D/g, ""); - } - }, { - key: "_trigger", - value: function _trigger(name) { - var e = new Event(name, { - bubbles: true, - cancelable: true - }); - this.telInput.dispatchEvent(e); - } - }, { - key: "_openDropdown", - value: function _openDropdown() { - var _this$options2 = this.options, fixDropdownWidth = _this$options2.fixDropdownWidth, countrySearch = _this$options2.countrySearch; - if (fixDropdownWidth) { - this.dropdownContent.style.width = "".concat(this.telInput.offsetWidth, "px"); - } - this.dropdownContent.classList.remove("iti__hide"); - this.selectedFlag.setAttribute("aria-expanded", "true"); - this._setDropdownPosition(); - // if we have previously selected a country (and countrySearch is disabled), then highlight that item and scroll to it - // else highlight the first item and scroll to top (even if countrySearch is disabled e.g. on init, showing globe icon) - if (this.activeItem && !countrySearch) { - // update highlighting and scroll to active list item - this._highlightListItem(this.activeItem, false); - this._scrollTo(this.activeItem, true); - } else { - // start by highlighting the first item in the list - var firstElementChild = this.countryList.firstElementChild; - if (firstElementChild) { - this._highlightListItem(firstElementChild, false); - this.countryList.scrollTop = 0; - } - if (countrySearch) { - this.searchInput.focus(); - } - } - // bind all the dropdown-related listeners: mouseover, click, click-off, keydown - this._bindDropdownListeners(); - // update the arrow - this.dropdownArrow.classList.add("iti__arrow--up"); - this._trigger("open:countrydropdown"); - } - }, { - key: "_toggleClass", - value: function _toggleClass(el, className, shouldHaveClass) { - if (shouldHaveClass && !el.classList.contains(className)) { - el.classList.add(className); - } else if (!shouldHaveClass && el.classList.contains(className)) { - el.classList.remove(className); - } - } - }, { - key: "_setDropdownPosition", - value: function _setDropdownPosition() { - var _this7 = this; - if (this.options.dropdownContainer) { - this.options.dropdownContainer.appendChild(this.dropdown); - } - if (!this.options.useFullscreenPopup) { - var pos = this.telInput.getBoundingClientRect(); - // windowTop from https://stackoverflow.com/a/14384091/217866 - var windowTop = document.documentElement.scrollTop; - var inputTop = pos.top + windowTop; - var dropdownHeight = this.dropdownContent.offsetHeight; - // dropdownFitsBelow = (dropdownBottom < windowBottom) - var dropdownFitsBelow = inputTop + this.telInput.offsetHeight + dropdownHeight < windowTop + window.innerHeight; - var dropdownFitsAbove = inputTop - dropdownHeight > windowTop; - // dont allow positioning above when country search enabled as the search box jumps around as you filter countries - var positionDropdownAboveInput = !this.options.countrySearch && !dropdownFitsBelow && dropdownFitsAbove; - // by default, the dropdown will be below the input. If we want to position it above the - // input, we add the dropup class. - this._toggleClass(this.dropdownContent, "iti__dropdown-content--dropup", positionDropdownAboveInput); - // if dropdownContainer is enabled, calculate postion - if (this.options.dropdownContainer) { - // if we want to position the dropdown below the input, we need to add the input height to the top value - var extraTop = positionDropdownAboveInput ? 0 : this.telInput.offsetHeight; - // calculate placement - this.dropdown.style.top = "".concat(inputTop + extraTop, "px"); - this.dropdown.style.left = "".concat(pos.left + document.body.scrollLeft, "px"); - // close menu on window scroll - this._handleWindowScroll = function() { - return _this7._closeDropdown(); - }; - window.addEventListener("scroll", this._handleWindowScroll); - } - } - } - }, { - key: "_bindDropdownListeners", - value: function _bindDropdownListeners() { - var _this8 = this; - // when mouse over a list item, just highlight that one - // we add the class "highlight", so if they hit "enter" we know which one to select - this._handleMouseoverCountryList = function(e) { - // handle event delegation, as we're listening for this event on the countryList - var listItem = e.target.closest(".iti__country"); - if (listItem) { - _this8._highlightListItem(listItem, false); - } - }; - this.countryList.addEventListener("mouseover", this._handleMouseoverCountryList); - // listen for country selection - this._handleClickCountryList = function(e) { - var listItem = e.target.closest(".iti__country"); - if (listItem) { - _this8._selectListItem(listItem); - } - }; - this.countryList.addEventListener("click", this._handleClickCountryList); - // click off to close - // (except when this initial opening click is bubbling up) - // we cannot just stopPropagation as it may be needed to close another instance - var isOpening = true; - this._handleClickOffToClose = function() { - if (!isOpening) { - _this8._closeDropdown(); - } - isOpening = false; - }; - document.documentElement.addEventListener("click", this._handleClickOffToClose); - // listen for up/down scrolling, enter to select, or escape to close - // use keydown as keypress doesn't fire for non-char keys and we want to catch if they - // just hit down and hold it to scroll down (no keyup event). - // listen on the document because that's where key events are triggered if no input has focus - var query = ""; - var queryTimer = null; - this._handleKeydownOnDropdown = function(e) { - // prevent down key from scrolling the whole page, - // and enter key from submitting a form etc - if ([ "ArrowUp", "ArrowDown", "Enter", "Escape" ].includes(e.key)) { - e.preventDefault(); - e.stopPropagation(); - // up and down to navigate - if (e.key === "ArrowUp" || e.key === "ArrowDown") { - _this8._handleUpDownKey(e.key); - } else if (e.key === "Enter") { - _this8._handleEnterKey(); - } else if (e.key === "Escape") { - _this8._closeDropdown(); - } - } - // alpha chars to perform search - // regex allows one latin alpha char or space, based on https://stackoverflow.com/a/26900132/217866) - if (!_this8.options.countrySearch && /^[a-zA-ZÀ-ÿа-яА-Я ]$/.test(e.key)) { - e.stopPropagation(); - // jump to countries that start with the query string - if (queryTimer) { - clearTimeout(queryTimer); - } - query += e.key.toLowerCase(); - _this8._searchForCountry(query); - // if the timer hits 1 second, reset the query - queryTimer = setTimeout(function() { - query = ""; - }, 1e3); - } - }; - document.addEventListener("keydown", this._handleKeydownOnDropdown); - if (this.options.countrySearch) { - var doFilter = function doFilter() { - var inputQuery = _this8.searchInput.value.trim(); - if (inputQuery) { - _this8._filterCountries(inputQuery); - } else { - _this8._filterCountries("", true); - } - }; - var keyupTimer = null; - this._handleSearchChange = function() { - // filtering country nodes is expensive (lots of DOM manipulation), so rate limit it - if (keyupTimer) { - clearTimeout(keyupTimer); - } - keyupTimer = setTimeout(function() { - doFilter(); - keyupTimer = null; - }, 100); - }; - this.searchInput.addEventListener("input", this._handleSearchChange); - // stop propagation on search input click, so doesn't trigger click-off-to-close listener - this.searchInput.addEventListener("click", function(e) { - return e.stopPropagation(); - }); - } - } - }, { - key: "_normaliseString", - value: function _normaliseString() { - var s = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ""; - return s.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase(); - } - }, { - key: "_filterCountries", - value: function _filterCountries(query) { - var isReset = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; - var isFirst = true; - this.countryList.innerHTML = ""; - var normalisedQuery = this._normaliseString(query); - for (var i = 0; i < this.countries.length; i++) { - var c = this.countries[i]; - var normalisedCountryName = this._normaliseString(c.name); - var fullDialCode = "+".concat(c.dialCode); - if (isReset || normalisedCountryName.includes(normalisedQuery) || fullDialCode.includes(normalisedQuery) || c.iso2.includes(normalisedQuery)) { - this.countryList.appendChild(c.nodeById[this.id]); - // highlight the first item - if (isFirst) { - this._highlightListItem(c.nodeById[this.id], false); - isFirst = false; - } - } - } - // scroll to top (useful if user had previously scrolled down) - this.countryList.scrollTop = 0; - this._updateSearchResultsText(); - } - }, { - key: "_updateSearchResultsText", - value: function _updateSearchResultsText() { - var i18n = this.options.i18n; - var count = this.countryList.childElementCount; - var searchText; - if (count === 0) { - searchText = i18n.zeroSearchResults || "No results found"; - } else if (count === 1) { - searchText = i18n.oneSearchResult || "1 result found"; - } else { - // eslint-disable-next-line no-template-curly-in-string - searchText = i18n.multipleSearchResults ? i18n.multipleSearchResults.replace("${count}", count) : "".concat(count, " results found"); - } - this.searchResultsA11yText.textContent = searchText; - } - }, { - key: "_handleUpDownKey", - value: function _handleUpDownKey(key) { - var next = key === "ArrowUp" ? this.highlightedItem.previousElementSibling : this.highlightedItem.nextElementSibling; - if (next) { - // skip the divider - if (next.classList.contains("iti__divider")) { - next = key === "ArrowUp" ? next.previousElementSibling : next.nextElementSibling; - } - } else if (this.countryList.childElementCount > 1) { - // otherwise, we must be at the end, so loop round again - next = key === "ArrowUp" ? this.countryList.lastElementChild : this.countryList.firstElementChild; - } - if (next) { - // make sure the next item is visible - // (before calling focus(), which can cause the next item to scroll to the middle of the dropdown, which is jarring) - this._scrollTo(next, false); - // if country search enabled, dont lose focus from the search input on up/down - var doFocus = !this.options.countrySearch; - this._highlightListItem(next, doFocus); - } - } - }, { - key: "_handleEnterKey", - value: function _handleEnterKey() { - if (this.highlightedItem) { - this._selectListItem(this.highlightedItem); - } - } - }, { - key: "_searchForCountry", - value: function _searchForCountry(query) { - for (var i = 0; i < this.countries.length; i++) { - if (this._startsWith(this.countries[i].name, query)) { - var listItem = this.countries[i].nodeById[this.id]; - // update highlighting and scroll - this._highlightListItem(listItem, false); - this._scrollTo(listItem, true); - break; - } - } - } - }, { - key: "_startsWith", - value: function _startsWith(a, b) { - return a.substr(0, b.length).toLowerCase() === b; - } - }, { - key: "_updateValFromNumber", - value: function _updateValFromNumber(fullNumber) { - var number = fullNumber; - if (this.options.formatOnDisplay && window.intlTelInputUtils && this.selectedCountryData) { - var useNational = this.options.nationalMode || number.charAt(0) !== "+" && !this.options.showSelectedDialCode; - var _intlTelInputUtils$nu = intlTelInputUtils.numberFormat, NATIONAL = _intlTelInputUtils$nu.NATIONAL, INTERNATIONAL = _intlTelInputUtils$nu.INTERNATIONAL; - var format = useNational ? NATIONAL : INTERNATIONAL; - number = intlTelInputUtils.formatNumber(number, this.selectedCountryData.iso2, format); - } - number = this._beforeSetNumber(number); - this.telInput.value = number; - } - }, { - key: "_updateFlagFromNumber", - value: function _updateFlagFromNumber(fullNumber) { - var plusIndex = fullNumber.indexOf("+"); - // if it contains a plus, discard any chars before it e.g. accidental space char. - // this keeps the selected country auto-updating correctly, which we want as - // libphonenumber's validation/getNumber methods will ignore these chars anyway - var number = plusIndex ? fullNumber.substring(plusIndex) : fullNumber; - // if we already have US/Canada selected, make sure the number starts - // with a +1 so _getDialCode will be able to extract the area code - // update: if we dont yet have selectedCountryData, but we're here (trying to update the flag - // from the number), that means we're initialising the plugin with a number that already has a - // dial code, so fine to ignore this bit - var selectedDialCode = this.selectedCountryData.dialCode; - var isNanp = selectedDialCode === "1"; - if (number && isNanp && number.charAt(0) !== "+") { - if (number.charAt(0) !== "1") { - number = "1".concat(number); - } - number = "+".concat(number); - } - // if showSelectedDialCode enabled, then consider the selected dial code to be part of the number - if (this.options.showSelectedDialCode && selectedDialCode && number.charAt(0) !== "+") { - number = "+".concat(selectedDialCode).concat(number); - } - // try and extract valid dial code from input - var dialCode = this._getDialCode(number, true); - var numeric = this._getNumeric(number); - var iso2 = null; - if (dialCode) { - var iso2Codes = this.dialCodeToIso2Map[this._getNumeric(dialCode)]; - // check if the right country is already selected. this should be false if the number is - // longer than the matched dial code because in this case we need to make sure that if - // there are multiple country matches, that the first one is selected (note: we could - // just check that here, but it requires the same loop that we already have later) - var alreadySelected = iso2Codes.indexOf(this.selectedCountryData.iso2) !== -1 && numeric.length <= dialCode.length - 1; - var isRegionlessNanpNumber = selectedDialCode === "1" && this._isRegionlessNanp(numeric); - // only update the flag if: - // A) NOT (we currently have a NANP flag selected, and the number is a regionlessNanp) - // AND - // B) the right country is not already selected - if (!isRegionlessNanpNumber && !alreadySelected) { - // if using onlyCountries option, iso2Codes[0] may be empty, so we must find the first - // non-empty index - for (var j = 0; j < iso2Codes.length; j++) { - if (iso2Codes[j]) { - iso2 = iso2Codes[j]; - break; - } - } - } - } else if (number.charAt(0) === "+" && numeric.length) { - // invalid dial code, so empty - // Note: use getNumeric here because the number has not been formatted yet, so could contain - // bad chars - iso2 = ""; - } else if ((!number || number === "+") && !this.selectedCountryData.iso2) { - // if no selected flag, and user either clears the input, or just types a plus, then show default - iso2 = this.defaultCountry; - } - if (iso2 !== null) { - return this._setCountry(iso2); - } - return false; - } - }, { - key: "_isRegionlessNanp", - value: function _isRegionlessNanp(number) { - var numeric = this._getNumeric(number); - if (numeric.charAt(0) === "1") { - var areaCode = numeric.substr(1, 3); - return regionlessNanpNumbers.indexOf(areaCode) !== -1; - } - return false; - } - }, { - key: "_highlightListItem", - value: function _highlightListItem(listItem, shouldFocus) { - var prevItem = this.highlightedItem; - if (prevItem) { - prevItem.classList.remove("iti__highlight"); - prevItem.setAttribute("aria-selected", "false"); - } - this.highlightedItem = listItem; - this.highlightedItem.classList.add("iti__highlight"); - this.highlightedItem.setAttribute("aria-selected", "true"); - this.selectedFlag.setAttribute("aria-activedescendant", listItem.getAttribute("id")); - if (this.options.countrySearch) { - this.searchInput.setAttribute("aria-activedescendant", listItem.getAttribute("id")); - } - if (shouldFocus) { - this.highlightedItem.focus(); - } - } - }, { - key: "_getCountryData", - value: function _getCountryData(iso2, allowFail) { - for (var i = 0; i < this.countries.length; i++) { - if (this.countries[i].iso2 === iso2) { - return this.countries[i]; - } - } - if (allowFail) { - return null; - } - throw new Error("No country data for '".concat(iso2, "'")); - } - }, { - key: "_setCountry", - value: function _setCountry(iso2) { - var _this$options3 = this.options, allowDropdown = _this$options3.allowDropdown, showSelectedDialCode = _this$options3.showSelectedDialCode, showFlags = _this$options3.showFlags, countrySearch = _this$options3.countrySearch, i18n = _this$options3.i18n; - var prevCountry = this.selectedCountryData.iso2 ? this.selectedCountryData : {}; - // do this first as it will throw an error and stop if iso2 is invalid - this.selectedCountryData = iso2 ? this._getCountryData(iso2, false) : {}; - // update the defaultCountry - we only need the iso2 from now on, so just store that - if (this.selectedCountryData.iso2) { - this.defaultCountry = this.selectedCountryData.iso2; - } - // update the flag class and the a11y text - if (this.selectedFlagInner) { - var flagClass = ""; - var a11yText = ""; - if (iso2) { - if (showFlags) { - flagClass = "iti__flag iti__".concat(iso2); - a11yText = "".concat(this.selectedCountryData.name, " +").concat(this.selectedCountryData.dialCode); - } - } else { - flagClass = "iti__flag iti__globe"; - a11yText = i18n.noCountrySelected || "No country selected"; - } - this.selectedFlagInner.className = flagClass; - this.selectedFlagA11yText.textContent = a11yText; - } - this._setSelectedCountryFlagTitleAttribute(iso2, showSelectedDialCode); - // update the selected dial code - if (showSelectedDialCode) { - var dialCode = this.selectedCountryData.dialCode ? "+".concat(this.selectedCountryData.dialCode) : ""; - this.selectedDialCode.innerHTML = dialCode; - // offsetWidth is zero if input is in a hidden container during initialisation - var selectedFlagWidth = this.selectedFlag.offsetWidth || this._getHiddenSelectedFlagWidth(); - // add 6px of padding after the grey selected-dial-code box, as this is what we use in the css - if (this.isRTL) { - this.telInput.style.paddingRight = "".concat(selectedFlagWidth + 6, "px"); - } else { - this.telInput.style.paddingLeft = "".concat(selectedFlagWidth + 6, "px"); - } - } - // and the input's placeholder - this._updatePlaceholder(); - // update the active list item (only if country search disabled, as country search doesn't store the active item) - if (allowDropdown && !countrySearch) { - var prevItem = this.activeItem; - if (prevItem) { - prevItem.classList.remove("iti__active"); - prevItem.setAttribute("aria-selected", "false"); - } - if (iso2) { - // check if there is a preferred item first, else fall back to standard - var nextItem = this.countryList.querySelector("#iti-".concat(this.id, "__item-").concat(iso2, "-preferred")) || this.countryList.querySelector("#iti-".concat(this.id, "__item-").concat(iso2)); - nextItem.setAttribute("aria-selected", "true"); - nextItem.classList.add("iti__active"); - this.activeItem = nextItem; - } - } - // return if the flag has changed or not - return prevCountry.iso2 !== iso2; - } - }, { - key: "_setSelectedCountryFlagTitleAttribute", - value: function _setSelectedCountryFlagTitleAttribute(iso2, showSelectedDialCode) { - if (!this.selectedFlag) { - return; - } - var title; - if (iso2 && !showSelectedDialCode) { - title = "".concat(this.selectedCountryData.name, ": +").concat(this.selectedCountryData.dialCode); - } else if (iso2) { - // For screen reader output, we don't want to include the dial code in the reader output twice - // so just use the selected country name here: - title = this.selectedCountryData.name; - } else { - title = "Unknown"; - } - this.selectedFlag.setAttribute("title", title); - } - }, { - key: "_getHiddenSelectedFlagWidth", - value: function _getHiddenSelectedFlagWidth() { - // to get the right styling to apply, all we need is a shallow clone of the container, - // and then to inject a deep clone of the selectedFlag element - var containerClone = this.telInput.parentNode.cloneNode(); - containerClone.style.visibility = "hidden"; - document.body.appendChild(containerClone); - var flagsContainerClone = this.flagsContainer.cloneNode(); - containerClone.appendChild(flagsContainerClone); - var selectedFlagClone = this.selectedFlag.cloneNode(true); - flagsContainerClone.appendChild(selectedFlagClone); - var width = selectedFlagClone.offsetWidth; - containerClone.parentNode.removeChild(containerClone); - return width; - } - }, { - key: "_updatePlaceholder", - value: function _updatePlaceholder() { - var _this$options4 = this.options, autoPlaceholder = _this$options4.autoPlaceholder, placeholderNumberType = _this$options4.placeholderNumberType, nationalMode = _this$options4.nationalMode, customPlaceholder = _this$options4.customPlaceholder; - var shouldSetPlaceholder = autoPlaceholder === "aggressive" || !this.hadInitialPlaceholder && autoPlaceholder === "polite"; - if (window.intlTelInputUtils && shouldSetPlaceholder) { - var numberType = intlTelInputUtils.numberType[placeholderNumberType]; - var placeholder = this.selectedCountryData.iso2 ? intlTelInputUtils.getExampleNumber(this.selectedCountryData.iso2, nationalMode, numberType) : ""; - placeholder = this._beforeSetNumber(placeholder); - if (typeof customPlaceholder === "function") { - placeholder = customPlaceholder(placeholder, this.selectedCountryData); - } - this.telInput.setAttribute("placeholder", placeholder); - } - } - }, { - key: "_selectListItem", - value: function _selectListItem(listItem) { - // update selected flag and active list item - var flagChanged = this._setCountry(listItem.getAttribute("data-country-code")); - this._closeDropdown(); - this._updateDialCode(listItem.getAttribute("data-dial-code")); - // focus the input - this.telInput.focus(); - if (flagChanged) { - this._triggerCountryChange(); - } - } - }, { - key: "_closeDropdown", - value: function _closeDropdown() { - this.dropdownContent.classList.add("iti__hide"); - this.selectedFlag.setAttribute("aria-expanded", "false"); - this.selectedFlag.removeAttribute("aria-activedescendant"); - if (this.highlightedItem) { - this.highlightedItem.setAttribute("aria-selected", "false"); - } - if (this.options.countrySearch) { - this.searchInput.removeAttribute("aria-activedescendant"); - } - // update the arrow - this.dropdownArrow.classList.remove("iti__arrow--up"); - // unbind key events - document.removeEventListener("keydown", this._handleKeydownOnDropdown); - if (this.options.countrySearch) { - this.searchInput.removeEventListener("input", this._handleSearchChange); - } - document.documentElement.removeEventListener("click", this._handleClickOffToClose); - this.countryList.removeEventListener("mouseover", this._handleMouseoverCountryList); - this.countryList.removeEventListener("click", this._handleClickCountryList); - // remove menu from container - if (this.options.dropdownContainer) { - if (!this.options.useFullscreenPopup) { - window.removeEventListener("scroll", this._handleWindowScroll); - } - if (this.dropdown.parentNode) { - this.dropdown.parentNode.removeChild(this.dropdown); - } - } - this._trigger("close:countrydropdown"); - } - }, { - key: "_scrollTo", - value: function _scrollTo(element, middle) { - var container = this.countryList; - // windowTop from https://stackoverflow.com/a/14384091/217866 - var windowTop = document.documentElement.scrollTop; - var containerHeight = container.offsetHeight; - var containerTop = container.getBoundingClientRect().top + windowTop; - var containerBottom = containerTop + containerHeight; - var elementHeight = element.offsetHeight; - var elementTop = element.getBoundingClientRect().top + windowTop; - var elementBottom = elementTop + elementHeight; - var newScrollTop = elementTop - containerTop + container.scrollTop; - var middleOffset = containerHeight / 2 - elementHeight / 2; - if (elementTop < containerTop) { - // scroll up - if (middle) { - newScrollTop -= middleOffset; - } - container.scrollTop = newScrollTop; - } else if (elementBottom > containerBottom) { - // scroll down - if (middle) { - newScrollTop += middleOffset; - } - var heightDifference = containerHeight - elementHeight; - container.scrollTop = newScrollTop - heightDifference; - } - } - }, { - key: "_updateDialCode", - value: function _updateDialCode(newDialCodeBare) { - var inputVal = this.telInput.value; - // save having to pass this every time - var newDialCode = "+".concat(newDialCodeBare); - var newNumber; - if (inputVal.charAt(0) === "+") { - // there's a plus so we're dealing with a replacement - var prevDialCode = this._getDialCode(inputVal); - if (prevDialCode) { - // current number contains a valid dial code, so replace it - newNumber = inputVal.replace(prevDialCode, newDialCode); - } else { - // current number contains an invalid dial code, so ditch it - // (no way to determine where the invalid dial code ends and the rest of the number begins) - newNumber = newDialCode; - } - this.telInput.value = newNumber; - } - } - }, { - key: "_getDialCode", - value: function _getDialCode(number, includeAreaCode) { - var dialCode = ""; - // only interested in international numbers (starting with a plus) - if (number.charAt(0) === "+") { - var numericChars = ""; - // iterate over chars - for (var i = 0; i < number.length; i++) { - var c = number.charAt(i); - // if char is number (https://stackoverflow.com/a/8935649/217866) - if (!isNaN(parseInt(c, 10))) { - numericChars += c; - // if current numericChars make a valid dial code - if (includeAreaCode) { - if (this.dialCodeToIso2Map[numericChars]) { - // store the actual raw string (useful for matching later) - dialCode = number.substr(0, i + 1); - } - } else { - if (this.dialCodes[numericChars]) { - dialCode = number.substr(0, i + 1); - // if we're just looking for a dial code, we can break as soon as we find one - break; - } - } - // stop searching as soon as we can - in this case when we hit max len - if (numericChars.length === this.dialCodeMaxLen) { - break; - } - } - } - } - return dialCode; - } - }, { - key: "_getFullNumber", - value: function _getFullNumber() { - var val = this.telInput.value.trim(); - var dialCode = this.selectedCountryData.dialCode; - var prefix; - var numericVal = this._getNumeric(val); - if (this.options.showSelectedDialCode && !this.options.nationalMode && val.charAt(0) !== "+" && dialCode && numericVal) { - // when using showSelectedDialCode, it is visible so is effectively part of the typed number - prefix = "+".concat(dialCode); - } else { - prefix = ""; - } - return prefix + val; - } - }, { - key: "_beforeSetNumber", - value: function _beforeSetNumber(fullNumber) { - var number = fullNumber; - if (this.options.showSelectedDialCode) { - var dialCode = this._getDialCode(number); - // if there is a valid dial code - if (dialCode) { - // in case _getDialCode returned an area code as well - dialCode = "+".concat(this.selectedCountryData.dialCode); - // a lot of numbers will have a space separating the dial code and the main number, and - // some NANP numbers will have a hyphen e.g. +1 684-733-1234 - in both cases we want to get - // rid of it - // NOTE: don't just trim all non-numerics as may want to preserve an open parenthesis etc - var start = number[dialCode.length] === " " || number[dialCode.length] === "-" ? dialCode.length + 1 : dialCode.length; - number = number.substr(start); - } - } - return this._cap(number); - } - }, { - key: "_triggerCountryChange", - value: function _triggerCountryChange() { - this._trigger("countrychange"); - } - }, { - key: "_formatNumberAsYouType", - value: function _formatNumberAsYouType() { - var val = this._getFullNumber(); - var result = window.intlTelInputUtils ? intlTelInputUtils.formatNumberAsYouType(val, this.selectedCountryData.iso2) : val; - // if showSelectedDialCode and they haven't (re)typed the dial code in the input as well, then remove the dial code - var dialCode = this.selectedCountryData.dialCode; - if (this.options.showSelectedDialCode && !this.options.nationalMode && this.telInput.value.charAt(0) !== "+" && result.includes("+".concat(dialCode))) { - var afterDialCode = result.split("+".concat(dialCode))[1] || ""; - return afterDialCode.trim(); - } - return result; - } - }, { - key: "handleAutoCountry", - value: function handleAutoCountry() { - if (this.options.initialCountry === "auto") { - // we must set this even if there is an initial val in the input: in case the initial val is - // invalid and they delete it - they should see their auto country - this.defaultCountry = window.intlTelInputGlobals.autoCountry; - // if there's no initial value in the input, then update the flag - if (!this.telInput.value) { - this.setCountry(this.defaultCountry); - } - this.resolveAutoCountryPromise(); - } - } - }, { - key: "handleUtils", - value: function handleUtils() { - // if the request was successful - if (window.intlTelInputUtils) { - // if there's an initial value in the input, then format it - if (this.telInput.value) { - this._updateValFromNumber(this.telInput.value); - } - if (this.selectedCountryData.iso2) { - this._updatePlaceholder(); - } - } - this.resolveUtilsScriptPromise(); - } - }, { - key: "destroy", - value: function destroy() { - if (this.options.allowDropdown) { - // make sure the dropdown is closed (and unbind listeners) - this._closeDropdown(); - this.selectedFlag.removeEventListener("click", this._handleClickSelectedFlag); - this.flagsContainer.removeEventListener("keydown", this._handleFlagsContainerKeydown); - // label click hack - var label = this.telInput.closest("label"); - if (label) { - label.removeEventListener("click", this._handleLabelClick); - } - } - // unbind hiddenInput listeners - var form = this.telInput.form; - if (this._handleHiddenInputSubmit && form) { - form.removeEventListener("submit", this._handleHiddenInputSubmit); - } - // unbind key events, and cut/paste events - this.telInput.removeEventListener("input", this._handleKeyEvent); - // remove attribute of id instance: data-intl-tel-input-id - this.telInput.removeAttribute("data-intl-tel-input-id"); - // remove markup (but leave the original input) - var wrapper = this.telInput.parentNode; - wrapper.parentNode.insertBefore(this.telInput, wrapper); - wrapper.parentNode.removeChild(wrapper); - delete window.intlTelInputGlobals.instances[this.id]; - } - }, { - key: "getExtension", - value: function getExtension() { - if (window.intlTelInputUtils) { - return intlTelInputUtils.getExtension(this._getFullNumber(), this.selectedCountryData.iso2); - } - return ""; - } - }, { - key: "getNumber", - value: function getNumber(format) { - if (window.intlTelInputUtils) { - var iso2 = this.selectedCountryData.iso2; - return intlTelInputUtils.formatNumber(this._getFullNumber(), iso2, format); - } - return ""; - } - }, { - key: "getNumberType", - value: function getNumberType() { - if (window.intlTelInputUtils) { - return intlTelInputUtils.getNumberType(this._getFullNumber(), this.selectedCountryData.iso2); - } - return -99; - } - }, { - key: "getSelectedCountryData", - value: function getSelectedCountryData() { - return this.selectedCountryData; - } - }, { - key: "getValidationError", - value: function getValidationError() { - if (window.intlTelInputUtils) { - var iso2 = this.selectedCountryData.iso2; - return intlTelInputUtils.getValidationError(this._getFullNumber(), iso2); - } - return -99; - } - }, { - key: "isValidNumber", - value: function isValidNumber() { - var mobileOnly = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; - var val = this._getFullNumber(); - // return false for any alpha chars - if (/(?:[A-Za-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0560-\u0588\u05D0-\u05EA\u05EF-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u0860-\u086A\u0870-\u0887\u0889-\u088E\u08A0-\u08C9\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u09FC\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C5D\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D04-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E86-\u0E8A\u0E8C-\u0EA3\u0EA5\u0EA7-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u1711\u171F-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1878\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4C\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1C90-\u1CBA\u1CBD-\u1CBF\u1CE9-\u1CEC\u1CEE-\u1CF3\u1CF5\u1CF6\u1CFA\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312F\u3131-\u318E\u31A0-\u31BF\u31F0-\u31FF\u3400-\u4DBF\u4E00-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA7CA\uA7D0\uA7D1\uA7D3\uA7D5-\uA7D9\uA7F2-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA8FE\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB69\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1F\uDF2D-\uDF40\uDF42-\uDF49\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF]|\uD801[\uDC00-\uDC9D\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDD70-\uDD7A\uDD7C-\uDD8A\uDD8C-\uDD92\uDD94\uDD95\uDD97-\uDDA1\uDDA3-\uDDB1\uDDB3-\uDDB9\uDDBB\uDDBC\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67\uDF80-\uDF85\uDF87-\uDFB0\uDFB2-\uDFBA]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE35\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE4\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2\uDD00-\uDD23\uDE80-\uDEA9\uDEB0\uDEB1\uDF00-\uDF1C\uDF27\uDF30-\uDF45\uDF70-\uDF81\uDFB0-\uDFC4\uDFE0-\uDFF6]|\uD804[\uDC03-\uDC37\uDC71\uDC72\uDC75\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD44\uDD47\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE2B\uDE3F\uDE40\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC5F-\uDC61\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE80-\uDEAA\uDEB8\uDF00-\uDF1A\uDF40-\uDF46]|\uD806[\uDC00-\uDC2B\uDCA0-\uDCDF\uDCFF-\uDD06\uDD09\uDD0C-\uDD13\uDD15\uDD16\uDD18-\uDD2F\uDD3F\uDD41\uDDA0-\uDDA7\uDDAA-\uDDD0\uDDE1\uDDE3\uDE00\uDE0B-\uDE32\uDE3A\uDE50\uDE5C-\uDE89\uDE9D\uDEB0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC72-\uDC8F\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD30\uDD46\uDD60-\uDD65\uDD67\uDD68\uDD6A-\uDD89\uDD98\uDEE0-\uDEF2\uDF02\uDF04-\uDF10\uDF12-\uDF33\uDFB0]|\uD808[\uDC00-\uDF99]|\uD809[\uDC80-\uDD43]|\uD80B[\uDF90-\uDFF0]|[\uD80C\uD81C-\uD820\uD822\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879\uD880-\uD883\uD885-\uD887][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2F\uDC41-\uDC46]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE70-\uDEBE\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDE40-\uDE7F\uDF00-\uDF4A\uDF50\uDF93-\uDF9F\uDFE0\uDFE1\uDFE3]|\uD821[\uDC00-\uDFF7]|\uD823[\uDC00-\uDCD5\uDD00-\uDD08]|\uD82B[\uDFF0-\uDFF3\uDFF5-\uDFFB\uDFFD\uDFFE]|\uD82C[\uDC00-\uDD22\uDD32\uDD50-\uDD52\uDD55\uDD64-\uDD67\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB]|\uD837[\uDF00-\uDF1E\uDF25-\uDF2A]|\uD838[\uDC30-\uDC6D\uDD00-\uDD2C\uDD37-\uDD3D\uDD4E\uDE90-\uDEAD\uDEC0-\uDEEB]|\uD839[\uDCD0-\uDCEB\uDFE0-\uDFE6\uDFE8-\uDFEB\uDFED\uDFEE\uDFF0-\uDFFE]|\uD83A[\uDC00-\uDCC4\uDD00-\uDD43\uDD4B]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDEDF\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF39\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0\uDFF0-\uDFFF]|\uD87B[\uDC00-\uDE5D]|\uD87E[\uDC00-\uDE1D]|\uD884[\uDC00-\uDF4A\uDF50-\uDFFF]|\uD888[\uDC00-\uDFAF])/.test(val)) { - return false; - } - return window.intlTelInputUtils ? intlTelInputUtils.isPossibleNumber(val, this.selectedCountryData.iso2, mobileOnly) : null; - } - }, { - key: "isValidNumberPrecise", - value: function isValidNumberPrecise() { - var val = this._getFullNumber(); - // return false for any alpha chars - if (/(?:[A-Za-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0560-\u0588\u05D0-\u05EA\u05EF-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u0860-\u086A\u0870-\u0887\u0889-\u088E\u08A0-\u08C9\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u09FC\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C5D\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D04-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E86-\u0E8A\u0E8C-\u0EA3\u0EA5\u0EA7-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u1711\u171F-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1878\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4C\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1C90-\u1CBA\u1CBD-\u1CBF\u1CE9-\u1CEC\u1CEE-\u1CF3\u1CF5\u1CF6\u1CFA\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312F\u3131-\u318E\u31A0-\u31BF\u31F0-\u31FF\u3400-\u4DBF\u4E00-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA7CA\uA7D0\uA7D1\uA7D3\uA7D5-\uA7D9\uA7F2-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA8FE\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB69\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1F\uDF2D-\uDF40\uDF42-\uDF49\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF]|\uD801[\uDC00-\uDC9D\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDD70-\uDD7A\uDD7C-\uDD8A\uDD8C-\uDD92\uDD94\uDD95\uDD97-\uDDA1\uDDA3-\uDDB1\uDDB3-\uDDB9\uDDBB\uDDBC\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67\uDF80-\uDF85\uDF87-\uDFB0\uDFB2-\uDFBA]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE35\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE4\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2\uDD00-\uDD23\uDE80-\uDEA9\uDEB0\uDEB1\uDF00-\uDF1C\uDF27\uDF30-\uDF45\uDF70-\uDF81\uDFB0-\uDFC4\uDFE0-\uDFF6]|\uD804[\uDC03-\uDC37\uDC71\uDC72\uDC75\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD44\uDD47\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE2B\uDE3F\uDE40\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC5F-\uDC61\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE80-\uDEAA\uDEB8\uDF00-\uDF1A\uDF40-\uDF46]|\uD806[\uDC00-\uDC2B\uDCA0-\uDCDF\uDCFF-\uDD06\uDD09\uDD0C-\uDD13\uDD15\uDD16\uDD18-\uDD2F\uDD3F\uDD41\uDDA0-\uDDA7\uDDAA-\uDDD0\uDDE1\uDDE3\uDE00\uDE0B-\uDE32\uDE3A\uDE50\uDE5C-\uDE89\uDE9D\uDEB0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC72-\uDC8F\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD30\uDD46\uDD60-\uDD65\uDD67\uDD68\uDD6A-\uDD89\uDD98\uDEE0-\uDEF2\uDF02\uDF04-\uDF10\uDF12-\uDF33\uDFB0]|\uD808[\uDC00-\uDF99]|\uD809[\uDC80-\uDD43]|\uD80B[\uDF90-\uDFF0]|[\uD80C\uD81C-\uD820\uD822\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879\uD880-\uD883\uD885-\uD887][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2F\uDC41-\uDC46]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE70-\uDEBE\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDE40-\uDE7F\uDF00-\uDF4A\uDF50\uDF93-\uDF9F\uDFE0\uDFE1\uDFE3]|\uD821[\uDC00-\uDFF7]|\uD823[\uDC00-\uDCD5\uDD00-\uDD08]|\uD82B[\uDFF0-\uDFF3\uDFF5-\uDFFB\uDFFD\uDFFE]|\uD82C[\uDC00-\uDD22\uDD32\uDD50-\uDD52\uDD55\uDD64-\uDD67\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB]|\uD837[\uDF00-\uDF1E\uDF25-\uDF2A]|\uD838[\uDC30-\uDC6D\uDD00-\uDD2C\uDD37-\uDD3D\uDD4E\uDE90-\uDEAD\uDEC0-\uDEEB]|\uD839[\uDCD0-\uDCEB\uDFE0-\uDFE6\uDFE8-\uDFEB\uDFED\uDFEE\uDFF0-\uDFFE]|\uD83A[\uDC00-\uDCC4\uDD00-\uDD43\uDD4B]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDEDF\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF39\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0\uDFF0-\uDFFF]|\uD87B[\uDC00-\uDE5D]|\uD87E[\uDC00-\uDE1D]|\uD884[\uDC00-\uDF4A\uDF50-\uDFFF]|\uD888[\uDC00-\uDFAF])/.test(val)) { - return false; - } - return window.intlTelInputUtils ? intlTelInputUtils.isValidNumber(val, this.selectedCountryData.iso2) : null; - } - }, { - key: "setCountry", - value: function setCountry(iso2) { - var iso2Lower = iso2.toLowerCase(); - // check if already selected - if (this.selectedCountryData.iso2 !== iso2Lower) { - this._setCountry(iso2Lower); - this._updateDialCode(this.selectedCountryData.dialCode); - this._triggerCountryChange(); - } - } - }, { - key: "setNumber", - value: function setNumber(number) { - // we must update the flag first, which updates this.selectedCountryData, which is used for - // formatting the number before displaying it - var flagChanged = this._updateFlagFromNumber(number); - this._updateValFromNumber(number); - if (flagChanged) { - this._triggerCountryChange(); - } - } - }, { - key: "setPlaceholderNumberType", - value: function setPlaceholderNumberType(type) { - this.options.placeholderNumberType = type; - this._updatePlaceholder(); - } - } ]); - return Iti; - }(); - /******************** - * STATIC METHODS - ********************/ - // get the country data object - intlTelInputGlobals.getCountryData = function() { - return allCountries; - }; - // inject a '; - - 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/tests/contracts/auth-error.test.ts b/tests/contracts/auth-error.test.ts new file mode 100644 index 0000000..c142abc --- /dev/null +++ b/tests/contracts/auth-error.test.ts @@ -0,0 +1,132 @@ +import { prepareAttempt } from "../helpers/auth-attempt"; +import { test, expect } from "vitest"; +import { startApp, freePort } from "../helpers/processes"; +import { configFixture } from "../helpers/config"; +import { startProvider } from "../helpers/provider"; +import { startRedis } from "../helpers/redis"; + +test("callback failures show safe actionable screens and retain machine-readable backchannel failure", async () => { + const provider = await startProvider(); + const redis = await startRedis(); + const config = configFixture({ + auth_servers: [{ id: "stage", url: provider.url }], + }); + const port = await freePort(); + const app = await startApp({ + NODE_ENV: "production", + REDIS_URL: redis.url, + SHOWCASE_KEY_PREFIX: "friendly-error-test:", + PORT: String(port), + NODE_CONFIG_DIR: config.dir, + }); + const cases = [ + [ + "token", + 401, + "invalid_client", + "CLIENT_AUTH_FAILED", + "Check the client credentials", + ], + ["token", 400, "invalid_grant", "AUTH_CODE_INVALID", "Start a new login"], + [ + "token", + 401, + "unrecognized-sensitive-error", + "TOKEN_REJECTED", + "rejected the token request", + ], + [ + "userinfo", + 401, + "invalid_token", + "USERINFO_FAILED", + "could not load your profile", + ], + [ + "token", + 503, + "server_error", + "PROVIDER_UNAVAILABLE", + "temporarily unavailable", + ], + ] as const; + try { + for (const [ + operation, + status, + providerCode, + reason, + explanation, + ] of cases) { + provider.behavior.failAt = operation; + provider.behavior.failStatus = status; + provider.behavior.failBody = { + error: providerCode, + error_description: + "RAW_PRIVATE_PROVIDER_TEXT fixture-secret ", + }; + const attempt = await prepareAttempt(app.url, "pvn_ip", true); + const response = await fetch( + app.url + + `/auth/callback/pvn_ip/stage?code=PRIVATE_AUTH_CODE&state=${attempt.state}`, + { redirect: "manual", headers: { cookie: attempt.callback } }, + ); + expect(response.status).toBe(302); + const destination = new URL(response.headers.get("location")!, app.url); + expect(destination.pathname).toBe("/auth/error"); + expect(destination.searchParams.get("reason")).toBe(reason); + expect(destination.searchParams.get("reference")).toMatch( + /^[0-9a-f-]{36}$/, + ); + const html = await (await fetch(destination)).text(); + expect(html).toContain(explanation); + expect(html).toContain("Try again"); + expect(html).toContain(destination.searchParams.get("reference")!); + expect(html).not.toContain("PRIVATE_AUTH_CODE"); + expect(html).not.toContain("RAW_PRIVATE_PROVIDER_TEXT"); + expect(html).not.toContain("fixture-secret"); + expect(destination.href).not.toContain("unrecognized-sensitive-error"); + } + const im = await prepareAttempt(app.url, "login_im"); + const backchannel = await fetch( + app.url + `/auth/callback/login_im/stage?code=c&state=${im.state}`, + { headers: { "ip-backchannel-im-auth": "true" }, redirect: "manual" }, + ); + expect(backchannel.status).toBe(400); + expect((await backchannel.json()).error).toBe("PROVIDER_UNAVAILABLE"); + provider.behavior.failAt = ""; + const fresh = await prepareAttempt(app.url); + const success = await fetch( + app.url + + `/auth/callback/pvn_ip/stage?code=fresh-fixture&state=${fresh.state}`, + { redirect: "manual", headers: { cookie: fresh.callback } }, + ); + expect(success.status).toBe(302); + expect(success.headers.get("location")).toBe( + app.url + `/auth/complete?state=${fresh.state}`, + ); + const completion = await fetch(success.headers.get("location")!, { + redirect: "manual", + headers: { cookie: fresh.owner }, + }); + expect(completion.headers.get("location")).toBe("/user/info"); + const unknown = await ( + await fetch( + app.url + "/auth/error?reason=%3Cscript%3E&reference=PRIVATE_AUTH_CODE", + ) + ).text(); + expect(unknown).toContain("Unable to complete authentication"); + // Next serializes URL search params into its router payload; only rendered diagnostics are filtered here. + expect(unknown.match(//)?.[0]).not.toContain( + "PRIVATE_AUTH_CODE", + ); + expect(app.output()).toContain('"event":"auth_callback_failed"'); + expect(app.output()).not.toContain("RAW_PRIVATE_PROVIDER_TEXT"); + expect(app.output()).not.toContain("fixture-secret"); + } finally { + await app.stop(); + config.remove(); + await provider.stop(); + await redis.stop(); + } +}); diff --git a/tests/contracts/bootstrap.test.ts b/tests/contracts/bootstrap.test.ts new file mode 100644 index 0000000..ef2f3e1 --- /dev/null +++ b/tests/contracts/bootstrap.test.ts @@ -0,0 +1,85 @@ +import { expect, test } from "vitest"; +import { request } from "node:http"; +import { randomBytes } from "node:crypto"; +import { networkInterfaces } from "node:os"; +function upgradeStatus(base: string, origin: string): Promise { + return new Promise((resolve) => { + const req = request(base + "/_next/hmr", { + headers: { + Connection: "Upgrade", + Upgrade: "websocket", + "Sec-WebSocket-Version": "13", + "Sec-WebSocket-Key": randomBytes(16).toString("base64"), + Origin: origin, + }, + }); + const timer = setTimeout(() => { + req.destroy(); + resolve(0); + }, 5000); + req.once("upgrade", (res, socket) => { + clearTimeout(timer); + socket.destroy(); + resolve(res.statusCode || 0); + }); + req.once("response", (res) => { + clearTimeout(timer); + res.resume(); + resolve(res.statusCode || 0); + }); + req.once("error", () => { + clearTimeout(timer); + resolve(0); + }); + req.end(); + }); +} +import { startApp } from "../helpers/processes"; +test("custom server serves Next pages, legacy root redirect and not-found", async () => { + const app = await startApp(); + try { + const port = new URL(app.url).port; + expect(app.output()).toContain( + `Local: http://localhost:${port}/auth/login`, + ); + expect(app.output()).toContain( + "For QR authentication from your phone, open a Network URL on your computer", + ); + const addresses = Object.values(networkInterfaces()) + .flatMap((values) => values || []) + .filter((entry) => entry.family === "IPv4" && !entry.internal) + .map((entry) => entry.address); + for (const address of new Set(addresses)) + expect(app.output()).toContain( + `Network: http://${address}:${port}/auth/login`, + ); + 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); + expect( + (await fetch(app.url + "/socket.io/?EIO=4&transport=polling")).status, + ).toBe(404); + // Dev hydration needs the HMR socket even when opened by IP from a phone. + const localHosts = new Set([ + "127.0.0.1", + ...Object.values(networkInterfaces()) + .flatMap((addresses) => addresses || []) + .filter((address) => address.family === "IPv4") + .map((address) => address.address), + ]); + for (const host of localHosts) { + expect(await upgradeStatus(app.url, `http://${host}:3002`), host).toBe( + 101, + ); + } + expect(await upgradeStatus(app.url, "https://untrusted.example")).not.toBe( + 101, + ); + } finally { + await app.stop(); + } +}); diff --git a/tests/contracts/ciba.test.ts b/tests/contracts/ciba.test.ts new file mode 100644 index 0000000..661f219 --- /dev/null +++ b/tests/contracts/ciba.test.ts @@ -0,0 +1,101 @@ +import { test, expect } from "vitest"; +import { startApp, freePort } from "../helpers/processes"; +import { startRedis } from "../helpers/redis"; +import { startProvider } from "../helpers/provider"; +import { configFixture } from "../helpers/config"; +test("CIBA endpoints preserve provider order, token error sessions and protocol payloads", async () => { + const redis = await startRedis(), + provider = await startProvider(); + const config = configFixture({ + auth_servers: [{ id: "stage", url: provider.url }], + }); + const port = await freePort(); + const app = await startApp({ + PORT: String(port), + NODE_CONFIG_DIR: config.dir, + REDIS_URL: redis.url, + }); + const post = (path: string, body: unknown) => + fetch(app.url + path, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(body), + }); + try { + for (const protocol of ["sms", "ts43"]) { + provider.calls.length = 0; + const auth = await post(`/${protocol}/auth`, { + client_id: "fixture-client", + login_hint: "12025550123", + server_id: "stage", + carrier_hint: 51010, + }); + expect(auth.status).toBe(200); + const body = await auth.json(); + expect(body.auth_req_id).toBe("fixture-auth-id"); + expect(body.nonce).toMatch(/^[a-f0-9-]{36}$/); + expect(provider.calls.map((call) => call.operation)).toEqual( + protocol === "sms" ? ["auth"] : ["auth", "dcql"], + ); + expect(provider.calls[0].body).toMatchObject({ + client_id: "fixture-client", + scope: "openid ip:phone_verify", + login_hint: "12025550123", + }); + if (protocol === "sms") { + expect(provider.calls[0].body.channel).toBe("sms"); + expect(provider.calls[0].body.message).toBe( + "Your verification PIN is: {{code}}", + ); + expect(provider.calls[0].body.carrier_hint).toBeUndefined(); + } else { + expect(body.digital_request.protocol).toBe("openid4vp-v1-unsigned"); + expect(provider.calls[0].body.carrier_hint).toBe("51010"); + expect(provider.calls[1].body).toEqual({ + operation: "VerifyPhoneNumber", + nonce: body.nonce, + }); + } + provider.calls.length = 0; + provider.behavior.failAt = "callback"; + const token = await post(`/${protocol}/token`, { + client_id: "fixture-client", + auth_req_id: body.auth_req_id, + nonce: body.nonce, + code: "123456", + vp_token: "fixture-vp", + }); + expect(token.status).toBe(200); + expect(provider.calls.map((c) => c.operation)).toEqual([ + "callback", + "token", + "userinfo", + ]); + expect(token.headers.get("set-cookie")).toContain("showcase_next_sid="); + expect(provider.calls[0].path).toBe( + `/realms/demo/protocol/openid-connect/ext/bc/${protocol}/callback`, + ); + expect(provider.calls[0].body).toEqual( + protocol === "sms" ? { code: "123456" } : { vp_token: "fixture-vp" }, + ); + expect(provider.calls[1].body).toMatchObject({ + auth_req_id: "fixture-auth-id", + grant_type: "urn:openid:params:grant-type:ciba", + }); + expect(provider.calls[2].body).toEqual({ access_token: "fixture-token" }); + provider.behavior.failAt = "token"; + const fail = await post(`/${protocol}/token`, { + client_id: "fixture-client", + auth_req_id: "a", + }); + expect(fail.status).toBe(503); + expect(fail.headers.get("set-cookie")).toContain("showcase_next_sid="); + provider.behavior.failAt = ""; + } + } finally { + await app.stop(); + config.remove(); + await provider.stop(); + await redis.stop(); + } +}); diff --git a/tests/contracts/config-geoip.test.ts b/tests/contracts/config-geoip.test.ts new file mode 100644 index 0000000..c8480f5 --- /dev/null +++ b/tests/contracts/config-geoip.test.ts @@ -0,0 +1,59 @@ +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, + }); + 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.version).toBe("1.2.17-SNAPSHOT"); + expect(data.clients[0].client_id).toBe("fixture-client"); + expect(data.clients[0].redirect_uri).toBe( + app.url + "/auth/callback/pvn_ip/stage", + ); + 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(); + } +}); + +for (const hops of [0, 1]) { + test(`actual Next server only trusts forwarded origin and IP with TRUST_PROXY_HOPS=${hops}`, async () => { + const config = configFixture(); + const app = await startApp({ + NODE_CONFIG_DIR: config.dir, + TRUST_PROXY_HOPS: String(hops), + }); + const headers = { + "x-forwarded-proto": "https", + "x-forwarded-host": "forwarded.example", + "x-forwarded-for": "203.0.113.7", + "x-showcase-peer-ip": "198.51.100.9", + }; + try { + const data = await ( + await fetch(app.url + "/api/config", { headers }) + ).json(); + expect(data.clients[0].redirect_uri).toBe( + `${hops ? "https://forwarded.example" : app.url}/auth/callback/pvn_ip/stage`, + ); + const geo = await ( + await fetch(app.url + "/api/geoip", { headers }) + ).json(); + expect(geo.ip).toBe(hops ? "203.0.113.7" : "127.0.0.1"); + } finally { + await app.stop(); + config.remove(); + } + }); +} diff --git a/tests/contracts/integrity.test.ts b/tests/contracts/integrity.test.ts new file mode 100644 index 0000000..da3d72e --- /dev/null +++ b/tests/contracts/integrity.test.ts @@ -0,0 +1,66 @@ +import { test, expect } from "vitest"; +import { startApp, freePort } from "../helpers/processes"; +import { startRedis } from "../helpers/redis"; +import { startProvider } from "../helpers/provider"; +import { configFixture } from "../helpers/config"; +test("Integrity bypass flow retains safe responses, bounded parser and single-use exchange", async () => { + const redis = await startRedis(), + provider = await startProvider(); + const config = configFixture({ + auth_servers: [{ id: "stage", url: provider.url }], + play_integrity_verify_domains: [], + }); + const port = await freePort(); + const app = await startApp({ + PORT: String(port), + NODE_CONFIG_DIR: config.dir, + REDIS_URL: redis.url, + PLAY_INTEGRITY_ENABLED: "true", + PLAY_INTEGRITY_PACKAGE_NAME: "com.fixture.app", + PLAY_INTEGRITY_USER_FLOW: "pvn_ip", + PLAY_INTEGRITY_STATE_SECRET: "fixture-state-secret", + PLAY_INTEGRITY_STATE_ISSUER: "fixture", + PLAY_INTEGRITY_STATE_AUDIENCE: "fixture", + }); + const post = (path: string, body: unknown) => + fetch(app.url + "/api/play-integrity/" + path, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(body), + }); + try { + const bad = await post("verify", { integrity_token: "x".repeat(17000) }); + expect(bad.status).toBe(400); + expect((await bad.json()).reason_codes).toEqual(["INVALID_REQUEST"]); + const attempt = await post("attempt", { + phone_number: "+12025550123", + server_id: "stage", + }); + expect(attempt.status).toBe(201); + const data = await attempt.json(); + expect(data.request_hash).toBeTruthy(); + const verify = await post("verify", { + attempt_id: data.attempt_id, + integrity_token: "opaque-fixture-token", + }); + expect(verify.status).toBe(201); + const { state } = await verify.json(); + const exchanged = await post("token-exchange", { + state, + code: "fixture-code", + }); + expect(exchanged.status).toBe(200); + expect((await exchanged.json()).decision).toBe("allow"); + expect( + (await post("token-exchange", { state, code: "fixture-code" })).status, + ).toBe(409); + expect(provider.calls.filter((c) => c.operation === "token")).toHaveLength( + 1, + ); + } finally { + await app.stop(); + config.remove(); + await provider.stop(); + await redis.stop(); + } +}); diff --git a/tests/contracts/mobile-s2s.test.ts b/tests/contracts/mobile-s2s.test.ts new file mode 100644 index 0000000..0dd61ca --- /dev/null +++ b/tests/contracts/mobile-s2s.test.ts @@ -0,0 +1,72 @@ +import { test, expect } from "vitest"; +import { startApp, freePort } from "../helpers/processes"; +import { startProvider } from "../helpers/provider"; +import { configFixture } from "../helpers/config"; +test("mobile exchanges submitted redirect URI, preserves userinfo precedence and creates no session", async () => { + const provider = await startProvider(); + provider.behavior.userInfo = { + auth_server: "provider-value", + sub: "fixture", + }; + const config = configFixture({ + auth_servers: [{ id: "stage", url: provider.url }], + }); + const port = await freePort(); + const app = await startApp({ + PORT: String(port), + NODE_CONFIG_DIR: config.dir, + }); + try { + const response = await fetch(app.url + "/auth/mobile/login", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + client_id: "fixture-client", + code: "mobile-code", + redirect_uri: "demo://callback", + }), + }); + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ + auth_server: "provider-value", + sub: "fixture", + }); + expect(response.headers.get("set-cookie")).toBeNull(); + expect(provider.calls[0].body.redirect_uri).toBe("demo://callback"); + const post = (body: Record) => + fetch(app.url + "/auth/mobile/login", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(body), + }); + provider.calls.length = 0; + const noServer = await post({ + client_id: "fixture-client", + server_id: "missing", + }); + expect(noServer.status).toBe(400); + expect(await noServer.json()).toEqual({ + error: "Invalid server_id: 'missing'", + }); + const noClient = await post({ client_id: "missing" }); + expect(noClient.status).toBe(401); + expect(await noClient.json()).toEqual({ error: "Client not found" }); + expect(provider.calls).toHaveLength(0); + provider.behavior.failAt = "token"; + const rejected = await post({ + client_id: "fixture-client", + code: "c", + redirect_uri: "demo://callback", + }); + expect(rejected.status).toBe(401); + expect(await rejected.json()).toEqual({ + error: "Request failed with status code 503", + }); + expect(rejected.headers.get("set-cookie")).toBeNull(); + expect(provider.calls.map((call) => call.operation)).toEqual(["token"]); + } finally { + await app.stop(); + config.remove(); + await provider.stop(); + } +}); diff --git a/tests/contracts/oauth-state.test.ts b/tests/contracts/oauth-state.test.ts new file mode 100644 index 0000000..326e6d4 --- /dev/null +++ b/tests/contracts/oauth-state.test.ts @@ -0,0 +1,208 @@ +import { test, expect } from "vitest"; +import { startApp } from "../helpers/processes"; +import { startRedis } from "../helpers/redis"; +import { startProvider } from "../helpers/provider"; +import { prepareAttempt } from "../helpers/auth-attempt"; +import { configFixture } from "../helpers/config"; + +test("web attempts bind initiator/scanner, reject callback and completion replay, and permit owner recovery", async () => { + const redis = await startRedis(), + provider = await startProvider(); + const config = configFixture({ + auth_servers: [{ id: "stage", url: provider.url }], + }); + const app = await startApp({ + NODE_CONFIG_DIR: config.dir, + REDIS_URL: redis.url, + SHOWCASE_KEY_PREFIX: "state-contract:", + }); + try { + const attempt = await prepareAttempt(app.url, "login_ip", true); + const owner = attempt.owner, + scanner = attempt.callback; + const callback = + app.url + `/auth/callback/login_ip/stage?state=${attempt.state}&code=c`; + const wrong = await fetch(callback, { redirect: "manual" }); + expect(wrong.headers.get("location")).toContain("AUTH_STATE_INVALID"); + expect(provider.calls).toHaveLength(0); + const results = await Promise.all( + [1, 2].map(() => + fetch(callback, { redirect: "manual", headers: { cookie: scanner } }), + ), + ); + expect( + results.filter( + (result) => result.headers.get("location") === "/auth/qrcode/complete", + ), + ).toHaveLength(1); + expect( + provider.calls.filter((call) => call.operation === "token"), + ).toHaveLength(1); + const status = await fetch( + app.url + `/auth/status?state=${attempt.state}`, + { headers: { cookie: owner } }, + ); + expect((await status.json()).status).toBe("completed"); + const unbound = await fetch( + app.url + `/auth/complete?state=${attempt.state}`, + { redirect: "manual", headers: { cookie: scanner } }, + ); + expect(unbound.headers.get("location")).toContain("AUTH_STATE_INVALID"); + const completions = await Promise.all( + [1, 2].map(() => + fetch(app.url + `/auth/complete?state=${attempt.state}`, { + redirect: "manual", + headers: { cookie: owner }, + }), + ), + ); + expect( + completions.filter( + (result) => result.headers.get("location") === "/user/info", + ), + ).toHaveLength(1); + // Wrong flow/server/origin and malformed/unprepared states never reach the provider. + const isolated = await prepareAttempt(app.url, "login_ip", true); + const count = provider.calls.length; + for (const path of [ + `/auth/callback/pvn_ip/stage?state=${isolated.state}&code=c`, + `/auth/callback/login_ip/stage?state=unprepared&code=c`, + `/auth/callback/login_ip/stage?state=${isolated.state}&state=duplicate&code=c`, + ]) { + const response = await fetch(app.url + path, { + redirect: "manual", + headers: { cookie: isolated.callback }, + }); + expect(response.headers.get("location")).toContain("AUTH_STATE_INVALID"); + } + expect(provider.calls).toHaveLength(count); + const expiredKey = "state-contract:oauth:" + isolated.state; + const record = JSON.parse((await redis.client.get(expiredKey))!); + await redis.client.set( + expiredKey, + JSON.stringify({ ...record, expiresAt: Date.now() - 1 }), + { PX: 60000 }, + ); + const expired = await fetch( + app.url + `/auth/callback/login_ip/stage?state=${isolated.state}&code=c`, + { redirect: "manual", headers: { cookie: isolated.callback } }, + ); + expect(expired.headers.get("location")).toContain("AUTH_STATE_EXPIRED"); + expect(provider.calls).toHaveLength(count); + + // A provider failure is recoverable by the QR owner, without granting a demo session. + const failed = await prepareAttempt(app.url, "login_ip", true); + provider.behavior.failAt = "token"; + const failure = await fetch( + app.url + `/auth/callback/login_ip/stage?state=${failed.state}&code=c`, + { redirect: "manual", headers: { cookie: failed.callback } }, + ); + expect(failure.headers.get("location")).toContain("/auth/error?"); + const recovered = await fetch( + app.url + `/auth/status?state=${failed.state}`, + { headers: { cookie: failed.owner } }, + ); + expect((await recovered.json()).status).toBe("failed"); + const failedCompletion = await fetch( + app.url + `/auth/complete?state=${failed.state}`, + { redirect: "manual", headers: { cookie: failed.owner } }, + ); + expect(failedCompletion.headers.get("location")).toContain("/auth/error?"); + expect(failedCompletion.headers.get("set-cookie")).toBeNull(); + + // Direct navigation must carry its own initiator binding. + const directResponse = await fetch(app.url + "/auth/prepare", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ user_flow: "login_ip", qr: false }), + }); + const direct = await directResponse.json(); + const stolen = await fetch(direct.url, { redirect: "manual" }); + expect(stolen.headers.get("location")).toContain("AUTH_STATE_INVALID"); + const crossSite = await fetch(app.url + "/auth/prepare", { + method: "POST", + headers: { + "content-type": "application/json", + origin: "https://attacker.example", + }, + body: JSON.stringify({ user_flow: "login_ip", qr: false }), + }); + expect(crossSite.status).toBe(400); + } finally { + await app.stop(); + config.remove(); + await provider.stop(); + await redis.stop(); + } +}); + +test("QR has two minutes to scan and a separate five-minute completion window", async () => { + const redis = await startRedis(), + provider = await startProvider(); + const config = configFixture({ + auth_servers: [{ id: "stage", url: provider.url }], + }); + const app = await startApp({ + NODE_CONFIG_DIR: config.dir, + REDIS_URL: redis.url, + SHOWCASE_KEY_PREFIX: "qr-window:", + }); + const prepare = async (qr: boolean) => { + const before = Date.now(); + const response = await fetch(app.url + "/auth/prepare", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ user_flow: "login_ip", qr }), + }); + const value = await response.json(); + return { ...value, before, cookie: response.headers.get("set-cookie")! }; + }; + try { + const qr = await prepare(true); + expect(qr.expiresAt - qr.before).toBeGreaterThanOrEqual(120000); + expect(qr.expiresAt - Date.now()).toBeLessThanOrEqual(120000); + expect(qr.cookie).toContain("Max-Age=420"); + const direct = await prepare(false); + expect(direct.expiresAt - direct.before).toBeGreaterThanOrEqual(300000); + expect(direct.cookie).toContain("Max-Age=300"); + const started = await fetch(qr.url, { redirect: "manual" }); + expect(started.headers.get("set-cookie")).toContain("Max-Age=300"); + const scanner = started.headers.get("set-cookie")!.split(";")[0]; + const owner = qr.cookie.split(";")[0]; + // Starting replaces the scan deadline with a fresh completion deadline. + const key = "qr-window:oauth:" + qr.state; + const record = JSON.parse((await redis.client.get(key))!); + expect(record.expiresAt - Date.now()).toBeGreaterThan(290000); + await redis.client.set( + key, + JSON.stringify({ ...record, scanExpiresAt: Date.now() - 1 }), + { PX: 600000 }, + ); + const status = await fetch(app.url + `/auth/status?state=${qr.state}`, { + headers: { cookie: owner }, + }); + const progress = await status.json(); + expect(progress.started).toBe(true); + expect(progress.expiresAt).toBe(record.expiresAt); + const complete = await fetch( + app.url + `/auth/callback/login_ip/stage?state=${qr.state}&code=c`, + { redirect: "manual", headers: { cookie: scanner } }, + ); + expect(complete.headers.get("location")).toBe("/auth/qrcode/complete"); + const expired = await prepare(true); + const expiredKey = "qr-window:oauth:" + expired.state; + const expiredRecord = JSON.parse((await redis.client.get(expiredKey))!); + await redis.client.set( + expiredKey, + JSON.stringify({ ...expiredRecord, expiresAt: Date.now() - 1 }), + { PX: 60000 }, + ); + const rejected = await fetch(expired.url, { redirect: "manual" }); + expect(rejected.headers.get("location")).toContain("AUTH_STATE_EXPIRED"); + } finally { + await app.stop(); + config.remove(); + await provider.stop(); + await redis.stop(); + } +}); diff --git a/tests/contracts/parity.test.ts b/tests/contracts/parity.test.ts new file mode 100644 index 0000000..733c285 --- /dev/null +++ b/tests/contracts/parity.test.ts @@ -0,0 +1,52 @@ +import { test, expect } from "vitest"; +import { startApp } from "../helpers/processes"; +import { startRedis } from "../helpers/redis"; +import { configFixture } from "../helpers/config"; + +test("HTTP endpoints reject unbound authentication and malformed requests", async () => { + const redis = await startRedis(); + const config = configFixture(); + const app = await startApp({ + NODE_CONFIG_DIR: config.dir, + REDIS_URL: redis.url, + PLAY_INTEGRITY_ENABLED: "false", + }); + try { + for (const route of [ + "/auth/callback/missing/stage", + "/auth/complete?state=missing-parity", + ]) { + const response = await fetch(app.url + route, { redirect: "manual" }); + expect(response.status, route).toBe(302); + expect(response.headers.get("location"), route).toContain( + "AUTH_STATE_INVALID", + ); + } + for (const route of [ + "/auth/s2s/signin", + "/auth/mobile/login", + "/sms/auth", + "/sms/token", + "/ts43/auth", + "/ts43/token", + "/sms/log", + "/ts43/log", + ]) { + for (const body of [ + "{", + JSON.stringify({ oversized: "x".repeat(102400) }), + ]) { + const response = await fetch(app.url + route, { + method: "POST", + headers: { "content-type": "application/json" }, + body, + }); + expect(response.status, `${route}: ${body.length} bytes`).toBe(500); + } + } + } finally { + await app.stop(); + config.remove(); + await redis.stop(); + } +}); diff --git a/tests/contracts/production-origin.test.ts b/tests/contracts/production-origin.test.ts new file mode 100644 index 0000000..4d6164d --- /dev/null +++ b/tests/contracts/production-origin.test.ts @@ -0,0 +1,78 @@ +import { test, expect } from "vitest"; +import { startApp } from "../helpers/processes"; +import { startRedis } from "../helpers/redis"; +import { configFixture } from "../helpers/config"; + +test("production derives QR and provider callback origins per request behind an HTTPS ingress", async () => { + const redis = await startRedis(); + const config = configFixture(); + let app: Awaited> | undefined; + try { + app = await startApp({ + NODE_ENV: "production", + NODE_CONFIG_DIR: config.dir, + TRUST_PROXY_HOPS: "1", + REDIS_URL: redis.url, + SHOWCASE_KEY_PREFIX: "production-origin:", + }); + for (const origin of [ + "https://showcase-one.example", + "https://showcase-two.example", + app.url, + ]) { + const url = new URL(origin); + const headers = { + "x-forwarded-host": url.host, + "x-forwarded-proto": url.protocol.slice(0, -1), + }; + const publicConfig = await ( + await fetch(app.url + "/api/config", { headers }) + ).json(); + expect(publicConfig.clients[0].redirect_uri).toBe( + origin + "/auth/callback/pvn_ip/stage", + ); + const prepared = await fetch(app.url + "/auth/prepare", { + method: "POST", + headers: { ...headers, origin, "content-type": "application/json" }, + body: JSON.stringify({ + user_flow: "login_ip", + server_id: "stage", + qr: true, + }), + }); + expect(prepared.status, await prepared.clone().text()).toBe(201); + const attempt = await prepared.json(); + expect(new URL(attempt.url).origin).toBe(origin); + const cookie = prepared.headers.get("set-cookie")!; + expect(cookie.includes("; Secure")).toBe(url.protocol === "https:"); + const start = await fetch( + app.url + new URL(attempt.url).pathname + new URL(attempt.url).search, + { + headers, + redirect: "manual", + }, + ); + expect(start.status).toBe(302); + const authorization = new URL(start.headers.get("location")!); + expect(authorization.searchParams.get("redirect_uri")).toBe( + origin + "/auth/callback/login_ip/stage", + ); + // An in-flight state cannot be moved onto another incoming domain. + const mismatched = await fetch( + app.url + "/auth/status?state=" + attempt.state, + { + headers: { + ...headers, + "x-forwarded-host": "different.example", + cookie: cookie.split(";")[0], + }, + }, + ); + expect(mismatched.status).toBe(400); + } + } finally { + await app?.stop(); + config.remove(); + await redis.stop(); + } +}); diff --git a/tests/contracts/web-auth.test.ts b/tests/contracts/web-auth.test.ts new file mode 100644 index 0000000..fa9d446 --- /dev/null +++ b/tests/contracts/web-auth.test.ts @@ -0,0 +1,229 @@ +import { prepareAttempt } from "../helpers/auth-attempt"; +import { test, expect } from "vitest"; +import { startApp, freePort } from "../helpers/processes"; +import { startRedis } from "../helpers/redis"; +import { startProvider } from "../helpers/provider"; +import { configFixture } from "../helpers/config"; +test("web callbacks retain 302, QR/backchannel order, independent session and S2S results", async () => { + const redis = await startRedis(), + provider = await startProvider(); + const config = configFixture({ + auth_servers: [{ id: "stage", url: provider.url }], + }); + const port = await freePort(), + origin = `http://127.0.0.1:${port}`; + const app = await startApp({ + PORT: String(port), + NODE_CONFIG_DIR: config.dir, + REDIS_URL: redis.url, + }); + const get = (path: string, options: RequestInit = {}) => + fetch(origin + path, { redirect: "manual", ...options }); + try { + const attempt = await prepareAttempt(origin); + const start = attempt.start; + expect(start.status).toBe(302); + expect( + new URL(start.headers.get("location")!).searchParams.get("redirect_uri"), + ).toBe(origin + "/auth/callback/pvn_ip/stage"); + const cb = await get( + `/auth/callback/pvn_ip/stage?code=fixture-code&state=${attempt.state}`, + { headers: { cookie: attempt.callback } }, + ); + expect(cb.status).toBe(302); + expect(cb.headers.get("location")).toBe( + origin + `/auth/complete?state=${attempt.state}`, + ); + expect(provider.calls.map((c) => c.operation)).toEqual([ + "token", + "userinfo", + ]); + const complete = await get(`/auth/complete?state=${attempt.state}`, { + headers: { cookie: attempt.owner }, + }); + expect(complete.status).toBe(302); + expect(complete.headers.get("location")).toBe("/user/info"); + const cookie = complete.headers.get("set-cookie")!.split(";")[0]; + expect((await get("/user/info", { headers: { cookie } })).status).toBe(200); + const anonymous = await get("/user/info"); + expect(anonymous.status, anonymous.status === 302 ? "" : app.output()).toBe( + 302, + ); + expect(anonymous.headers.get("location")).toBe("/"); + const s2s = await get("/auth/s2s/signin", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ state: attempt.state }), + }); + expect(s2s.status).toBe(200); + expect(await s2s.json()).toEqual(provider.behavior.userInfo); + expect(s2s.headers.get("set-cookie")).toBeNull(); + const repeat = await get("/auth/s2s/signin", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ state: attempt.state }), + }); + expect(await repeat.json()).toEqual(provider.behavior.userInfo); + const missing = await get("/auth/s2s/signin", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ state: "missing" }), + }); + expect(missing.status).toBe(401); + expect(await missing.text()).toBe(""); + const pendingIm = await prepareAttempt(origin, "login_im"); + provider.behavior.tokenDelay = 500; + const inFlight = get( + `/auth/callback/login_im/stage?code=c&state=${pendingIm.state}`, + { headers: { "ip-backchannel-im-auth": "true" } }, + ); + for ( + let i = 0; + i < 100 && + provider.calls.filter((c) => c.operation === "token").length < 2; + i++ + ) + await new Promise((r) => setTimeout(r, 10)); + const pendingReturn = await get( + `/auth/callback/login_im/stage?code=c&state=${pendingIm.state}`, + { headers: { cookie: pendingIm.callback } }, + ); + expect(pendingReturn.headers.get("location")).toBe( + `/auth/pending?state=${pendingIm.state}`, + ); + expect(pendingReturn.headers.get("set-cookie")).toBeNull(); + expect((await inFlight).status).toBe(200); + provider.behavior.tokenDelay = 0; + const im = await prepareAttempt(origin, "login_im"); + const back = await get( + `/auth/callback/login_im/stage?code=c&state=${im.state}`, + { headers: { "ip-backchannel-im-auth": "true" } }, + ); + expect(back.status).toBe(200); + expect(await back.text()).toBe(""); + expect(back.headers.get("set-cookie")).toBeNull(); + const resumed = await get( + `/auth/callback/login_im/stage?code=c&state=${im.state}`, + { headers: { cookie: im.callback } }, + ); + expect(resumed.headers.get("location")).toBe( + origin + `/auth/complete?state=${im.state}`, + ); + const qr = await prepareAttempt(origin, "pvn_ip", true); + provider.behavior.userInfo = { phone_number_verified: "false" }; + expect( + ( + await get(`/auth/callback/pvn_ip/stage?code=c&state=${qr.state}`, { + headers: { cookie: qr.callback }, + }) + ).headers.get("location"), + ).toBe("/auth/qrcode/error"); + const qr2 = await prepareAttempt(origin, "pvn_ip", true); + provider.behavior.userInfo = { phone_number_verified: false }; + expect( + ( + await get(`/auth/callback/pvn_ip/stage?code=c&state=${qr2.state}`, { + headers: { cookie: qr2.callback }, + }) + ).headers.get("location"), + ).toBe("/auth/qrcode/complete"); + } finally { + await app.stop(); + config.remove(); + await provider.stop(); + await redis.stop(); + } +}); + +test("callback errors reach the bound browser sessions without exposing them in redirects", async () => { + const redis = await startRedis(), + provider = await startProvider(); + const config = configFixture({ + auth_servers: [{ id: "stage", url: provider.url }], + }); + const port = await freePort(), + origin = `http://127.0.0.1:${port}`; + const app = await startApp({ + PORT: String(port), + NODE_CONFIG_DIR: config.dir, + REDIS_URL: redis.url, + }); + const get = (path: string, cookie?: string) => + fetch(origin + path, { + redirect: "manual", + headers: cookie ? { cookie } : {}, + }); + const sessionCookie = (response: Response) => + response.headers + .getSetCookie() + .find((value) => value.startsWith("showcase_next_sid="))! + .split(";")[0]; + try { + for (const [params, expected] of [ + [ + { + error: "msisdn_country_mismatch", + error_description: "Country mismatch", + code: "ignored", + }, + "msisdn_country_mismatch", + ], + [ + { error_description: "Provider could not verify this number" }, + "Provider could not verify this number", + ], + [ + { + error_description: + "Failed code=secret Bearer token https://example.com/private", + }, + "Failed [sensitive data removed] [sensitive data removed] [link removed]", + ], + [{}, "Unable to complete authentication."], + ] as Array<[Record, string]>) { + const attempt = await prepareAttempt(origin); + const query = new URLSearchParams({ state: attempt.state, ...params }); + const callback = await get( + `/auth/callback/pvn_ip/stage?${query}`, + attempt.callback, + ); + expect(callback.status).toBe(302); + expect(callback.headers.get("location")).toBe("/auth/login"); + const cookie = sessionCookie(callback); + const error = await get("/auth/login/error", cookie); + expect((await error.json()).error_message).toBe(expected); + const consumed = await get("/auth/login/error", cookie); + expect((await consumed.json()).error_message).toBeNull(); + } + const qr = await prepareAttempt(origin, "pvn_ip", true); + const query = new URLSearchParams({ + state: qr.state, + error: "msisdn_country_mismatch", + }); + const callback = await get( + `/auth/callback/pvn_ip/stage?${query}`, + qr.callback, + ); + expect(callback.headers.get("location")).toBe("/auth/qrcode/error"); + const scannerPage = await get( + "/auth/qrcode/error", + sessionCookie(callback), + ); + expect(await scannerPage.text()).toContain("msisdn_country_mismatch"); + const status = await get(`/auth/status?state=${qr.state}`, qr.owner); + const statusBody = await status.json(); + expect(statusBody.status).toBe("failed"); + const complete = await get(`/auth/complete?state=${qr.state}`, qr.owner); + expect(complete.headers.get("location")).toBe("/auth/login"); + const ownerError = await get("/auth/login/error", sessionCookie(complete)); + expect((await ownerError.json()).error_message).toBe( + "msisdn_country_mismatch", + ); + expect(provider.calls).toEqual([]); + } finally { + await app.stop(); + config.remove(); + await provider.stop(); + await redis.stop(); + } +}); diff --git a/tests/helpers/auth-attempt.ts b/tests/helpers/auth-attempt.ts new file mode 100644 index 0000000..af60612 --- /dev/null +++ b/tests/helpers/auth-attempt.ts @@ -0,0 +1,28 @@ +import { expect } from "vitest"; +export async function prepareAttempt( + base: string, + flow = "pvn_ip", + qr = false, +) { + const response = await fetch(base + "/auth/prepare", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ user_flow: flow, server_id: "stage", qr }), + }); + expect(response.status).toBe(201); + const data = await response.json(); + const owner = response.headers.get("set-cookie")!.split(";")[0]; + const start = await fetch(data.url, { + redirect: "manual", + headers: { cookie: owner }, + }); + expect(start.status).toBe(302); + const callback = start.headers.get("set-cookie")!.split(";")[0]; + return { ...data, owner, callback, start } as { + state: string; + url: string; + owner: string; + callback: string; + start: Response; + }; +} diff --git a/tests/helpers/config.ts b/tests/helpers/config.ts new file mode 100644 index 0000000..a7aad9c --- /dev/null +++ b/tests/helpers/config.ts @@ -0,0 +1,29 @@ +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_im", scope: "openid", title: "IM Login" }, + { 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/tests/helpers/e2e-server.mjs b/tests/helpers/e2e-server.mjs new file mode 100644 index 0000000..9e7548f --- /dev/null +++ b/tests/helpers/e2e-server.mjs @@ -0,0 +1,127 @@ +import { spawn } from "node:child_process"; +import { createServer } from "node:http"; +import { createServer as createNetServer } from "node:net"; +import { mkdtempSync, readFileSync, writeFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +const port = process.env.E2E_PORT || "3002"; +const allocator = createNetServer(); +await new Promise((r) => allocator.listen(0, "127.0.0.1", r)); +const redisPort = allocator.address().port; +await new Promise((r) => allocator.close(r)); +const redis = spawn( + "redis-server", + [ + "--bind", + "127.0.0.1", + "--port", + String(redisPort), + "--save", + "", + "--appendonly", + "no", + ], + { stdio: "ignore" }, +); +const resultClaims = { + "fixture-result-missing": undefined, + "fixture-result-boolean-true": true, + "fixture-result-boolean-false": false, + "fixture-result-string-true": "true", + "fixture-result-string-false": "false", +}; +const provider = createServer(async (req, res) => { + const u = new URL(req.url, "http://provider"); + if (req.method === "GET" && u.pathname.endsWith("/auth")) { + const target = new URL(u.searchParams.get("redirect_uri")); + target.searchParams.set("code", "browser-fixture-code"); + target.searchParams.set("state", u.searchParams.get("state")); + res.writeHead(302, { Location: target.href }); + res.end(); + return; + } + let raw = ""; + for await (const part of req) raw += part; + const body = req.headers["content-type"]?.includes("application/json") + ? JSON.parse(raw || "{}") + : Object.fromEntries(new URLSearchParams(raw)); + res.setHeader("Content-Type", "application/json"); + if (u.pathname.endsWith("/token") && body.code === "browser-invalid-client") { + res.writeHead(401); + res.end( + JSON.stringify({ + error: "invalid_client", + error_description: "PRIVATE_PROVIDER_DIAGNOSTIC", + }), + ); + } else if (u.pathname.endsWith("/ciba/auth")) + res.end(JSON.stringify({ auth_req_id: "browser-auth-id" })); + else if (u.pathname.endsWith("/dcql")) + res.end(JSON.stringify({ id: "phone", format: "ts43" })); + else if (u.pathname.endsWith("/callback") && body.code === "000000") { + res.writeHead(400); + res.end(JSON.stringify({ error: "invalid_code" })); + } else if (u.pathname.endsWith("/token") && body.auth_req_id === "bad") { + res.writeHead(400); + res.end(JSON.stringify({ error: "invalid_code" })); + } else if (u.pathname.endsWith("/token")) + res.end( + JSON.stringify({ + access_token: Object.hasOwn(resultClaims, body.code) + ? body.code + : "browser-token", + }), + ); + else + res.end( + JSON.stringify({ + sub: "browser-user", + phone_number: "+12025550123", + phone_number_verified: Object.hasOwn(resultClaims, body.access_token) + ? resultClaims[body.access_token] + : "true", + ...(Object.hasOwn(resultClaims, body.access_token) + ? { custom_claim: { enabled: false, count: 0, value: null } } + : {}), + }), + ); +}); +await new Promise((r) => provider.listen(0, "127.0.0.1", r)); +const dir = mkdtempSync(path.join(tmpdir(), "showcase-browser-")); +const config = JSON.parse(readFileSync("config/default.json.sample", "utf8")); +Object.assign(config, { + client_id: "browser-client", + client_secret: "browser-fixture-secret", + default_country_code: "us", + auth_servers: [ + { id: "stage", url: `http://127.0.0.1:${provider.address().port}` }, + { id: "secondary", url: `http://127.0.0.1:${provider.address().port}` }, + ], + locale: { app: { name: "IPification Showcase" } }, +}); +writeFileSync(path.join(dir, "default.json"), JSON.stringify(config)); +const child = spawn(process.execPath, ["server.mjs"], { + env: { + ...process.env, + NODE_ENV: "production", + PORT: port, + APP_ENV: "development", + NODE_CONFIG_ENV: "development", + NODE_CONFIG_DIR: dir, + REDIS_URL: `redis://127.0.0.1:${redisPort}`, + SHOWCASE_KEY_PREFIX: "showcase-browser:", + }, + stdio: "inherit", +}); +function shutdown() { + child.kill("SIGTERM"); + redis.kill("SIGTERM"); + provider.closeAllConnections(); + provider.close(); + rmSync(dir, { recursive: true, force: true }); +} +for (const signal of ["SIGTERM", "SIGINT"]) process.once(signal, shutdown); +child.once("exit", (code) => { + shutdown(); + process.exit(code ?? 0); +}); diff --git a/tests/helpers/processes.ts b/tests/helpers/processes.ts new file mode 100644 index 0000000..ebd8b1f --- /dev/null +++ b/tests/helpers/processes.ts @@ -0,0 +1,48 @@ +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/tests/helpers/provider.ts b/tests/helpers/provider.ts new file mode 100644 index 0000000..e15cfc6 --- /dev/null +++ b/tests/helpers/provider.ts @@ -0,0 +1,69 @@ +import { createServer } from "node:http"; +export async function startProvider() { + const calls: { + operation: string; + body: Record; + path: string; + }[] = []; + const responded: string[] = []; + const behavior = { + userInfo: { + phone_number: "+12025550123", + phone_number_verified: "true", + } as Record, + delays: {} as Record, + tokenDelay: 0, + failAt: "", + failStatus: 503, + failBody: { error: "provider-unavailable" } as Record, + tokenResponse: { access_token: "fixture-token" } as unknown, + }; + const server = createServer(async (req, res) => { + let raw = ""; + for await (const chunk of req) raw += chunk; + const body = req.headers["content-type"]?.includes("application/json") + ? JSON.parse(raw || "{}") + : Object.fromEntries(new URLSearchParams(raw)); + const path = req.url || ""; + const operation = path.endsWith("/ciba/auth") + ? "auth" + : path.endsWith("/dcql") + ? "dcql" + : path.endsWith("/callback") + ? "callback" + : path.endsWith("/token") + ? "token" + : "userinfo"; + calls.push({ operation, body, path }); + const delay = + behavior.delays[operation] || + (operation === "token" ? behavior.tokenDelay : 0); + if (delay) await new Promise((r) => setTimeout(r, delay)); + responded.push(operation); + res.setHeader("Content-Type", "application/json"); + if (behavior.failAt === operation) { + res.writeHead(behavior.failStatus); + res.end(JSON.stringify(behavior.failBody)); + return; + } + res.end( + JSON.stringify( + { + auth: { auth_req_id: "fixture-auth-id" }, + dcql: { id: "phone", format: "ts43" }, + callback: { ok: true }, + token: behavior.tokenResponse, + userinfo: behavior.userInfo, + }[operation], + ), + ); + }); + await new Promise((r) => server.listen(0, "127.0.0.1", r)); + return { + url: `http://127.0.0.1:${(server.address() as any).port}`, + calls, + responded, + behavior, + stop: () => new Promise((r) => server.close(() => r())), + }; +} diff --git a/tests/helpers/redis.ts b/tests/helpers/redis.ts new file mode 100644 index 0000000..c3ca24b --- /dev/null +++ b/tests/helpers/redis.ts @@ -0,0 +1,45 @@ +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 () => { + if (client.isOpen) client.destroy(); + await stop(child); + }, + }; +} diff --git a/tests/helpers/server-only.ts b/tests/helpers/server-only.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/tests/helpers/server-only.ts @@ -0,0 +1 @@ +export {}; diff --git a/tests/helpers/service-context.ts b/tests/helpers/service-context.ts new file mode 100644 index 0000000..fc7614a --- /dev/null +++ b/tests/helpers/service-context.ts @@ -0,0 +1,24 @@ +import type { ServerConfig } from "@/server/config/load"; +export const serviceContext = { + origin: "https://showcase.example", + hostname: "showcase.example", + appEnv: "stage", + clientIp: "203.0.113.1", + config: { + realm: "demo", + client_id: "client", + client_secret: "secret", + auth_servers: [{ id: "stage", url: "https://provider.example" }], + clients: [ + { + user_flow: "login_ip", + client_id: "client", + client_secret: "secret", + scope: "openid", + title: "Login", + }, + ], + locale: {}, + app_config: {}, + } satisfies ServerConfig, +}; diff --git a/tests/integration/auth-results.test.ts b/tests/integration/auth-results.test.ts new file mode 100644 index 0000000..7fb1618 --- /dev/null +++ b/tests/integration/auth-results.test.ts @@ -0,0 +1,42 @@ +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, + ); + await redis.client.pExpire("result-test:result:s", 100); + await redis.client.pExpire("result-test:result:s-user-info", 100); + await waitForResult("s"); + expect(await redis.client.pTTL("result-test:result:s")).toBeLessThanOrEqual( + 100, + ); + await new Promise((resolve) => setTimeout(resolve, 150)); + expect(await getUserInfo("s")).toBeUndefined(); + } finally { + await closeRedis(); + await redis.stop(); + } +}); diff --git a/tests/integration/config-runtime.test.ts b/tests/integration/config-runtime.test.ts new file mode 100644 index 0000000..e24d7eb --- /dev/null +++ b/tests/integration/config-runtime.test.ts @@ -0,0 +1,31 @@ +import { test, expect } from "vitest"; +import { spawnSync } from "node:child_process"; +import { configFixture } from "../helpers/config"; +import { tmpdir } from "node:os"; +import path from "node:path"; +test("config checker validates mounted config without Redis or provider calls", () => { + const c = configFixture(); + try { + const run = (dir: string) => + spawnSync(process.execPath, [path.resolve("scripts/check-config.mjs")], { + cwd: tmpdir(), + env: { + ...process.env, + NODE_ENV: "production", + APP_ENV: "stage", + NODE_CONFIG_ENV: "stage", + NODE_CONFIG_DIR: dir, + REDIS_URL: "redis://127.0.0.1:1", + }, + encoding: "utf8", + }); + const good = run(c.dir); + expect(good.status, good.stderr).toBe(0); + expect(good.stdout).not.toContain("fixture-secret"); + const bad = run(path.join(c.dir, "missing")); + expect(bad.status).not.toBe(0); + expect(bad.stderr).not.toContain("fixture-secret"); + } finally { + c.remove(); + } +}); diff --git a/tests/integration/cross-instance.test.ts b/tests/integration/cross-instance.test.ts new file mode 100644 index 0000000..f0dbcdc --- /dev/null +++ b/tests/integration/cross-instance.test.ts @@ -0,0 +1,137 @@ +import { prepareAttempt } from "../helpers/auth-attempt"; +import { test, expect } from "vitest"; +import { startApp, freePort } from "../helpers/processes"; +import { startRedis } from "../helpers/redis"; +import { startProvider } from "../helpers/provider"; +import { configFixture } from "../helpers/config"; +test("production instances share callbacks, sessions across restart, and single-use Integrity claims", async () => { + const redis = await startRedis(), + provider = await startProvider(); + const config = configFixture({ + auth_servers: [{ id: "stage", url: provider.url }], + }); + const portA = await freePort(), + portB = await freePort(); + const env = { + NODE_ENV: "production", + TRUST_PROXY_HOPS: "1", + NODE_CONFIG_DIR: config.dir, + REDIS_URL: redis.url, + SHOWCASE_KEY_PREFIX: "cross-instance:", + PLAY_INTEGRITY_ENABLED: "true", + PLAY_INTEGRITY_PACKAGE_NAME: "com.fixture.app", + PLAY_INTEGRITY_USER_FLOW: "pvn_ip", + PLAY_INTEGRITY_STATE_SECRET: "fixture-state-secret", + PLAY_INTEGRITY_STATE_ISSUER: "fixture", + PLAY_INTEGRITY_STATE_AUDIENCE: "fixture", + }; + // Both replicas see the same public host supplied by an ingress. + function fetchInstance(url: string, init: RequestInit = {}) { + const headers = new Headers(init.headers); + headers.set("x-forwarded-host", `127.0.0.1:${portA}`); + headers.set("x-forwarded-proto", "http"); + return fetch(url, { ...init, headers }); + } + const a = await startApp({ ...env, PORT: String(portA) }); + let b = await startApp({ ...env, PORT: String(portB) }); + try { + const attempt = await prepareAttempt(a.url, "pvn_ip", true); + const callback = await fetchInstance( + b.url + `/auth/callback/pvn_ip/stage?code=c&state=${attempt.state}`, + { redirect: "manual", headers: { cookie: attempt.callback } }, + ); + expect(callback.status).toBe(302); + // Owner status reads work on either replica, with no transport affinity. + for (const base of [a.url, b.url]) { + const status = await fetchInstance( + base + `/auth/status?state=${attempt.state}`, + { + headers: { cookie: attempt.owner }, + }, + ); + expect(status.status).toBe(200); + expect(await status.json()).toMatchObject({ + status: "completed", + completionUrl: a.url + `/auth/complete?state=${attempt.state}`, + }); + const unauthorized = await fetchInstance( + base + `/auth/status?state=${attempt.state}`, + ); + expect(unauthorized.status).toBe(400); + } + const complete = await fetchInstance( + a.url + `/auth/complete?state=${attempt.state}`, + { + redirect: "manual", + headers: { cookie: attempt.owner }, + }, + ); + const cookie = complete.headers.get("set-cookie")!.split(";")[0]; + const result = await fetchInstance(b.url + "/user/info", { + headers: { cookie }, + }); + expect(result.status).toBe(200); + expect(await result.text()).toContain("+12025550123"); + expect((await fetchInstance(a.url + "/health/ready")).status).toBe(200); + await b.stop(); + b = await startApp({ ...env, PORT: String(portB) }); + expect( + (await fetchInstance(b.url + "/user/info", { headers: { cookie } })) + .status, + ).toBe(200); + const post = (base: string, endpoint: string, body: unknown) => + fetchInstance(base + endpoint, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(body), + }); + // Results survive restart and S2S reads do not consume them. + for (const base of [a.url, b.url, b.url]) { + const response = await post(base, "/auth/s2s/signin", { + state: attempt.state, + }); + expect((await response.json()).phone_number).toBe("+12025550123"); + } + const created = await post(a.url, "/api/play-integrity/attempt", { + phone_number: "+12025550123", + server_id: "stage", + }); + expect(created.status).toBe(201); + const integrityAttempt = await created.json(); + const verifications = await Promise.all( + [a.url, b.url].map((base) => + post(base, "/api/play-integrity/verify", { + attempt_id: integrityAttempt.attempt_id, + integrity_token: "fixture-token", + }), + ), + ); + expect(verifications.map((r) => r.status).sort()).toEqual([201, 409]); + const { state } = await verifications.find((r) => r.status === 201)!.json(); + const exchanges = await Promise.all( + [a.url, b.url].map((base) => + post(base, "/api/play-integrity/token-exchange", { + state, + code: "fixture-code", + }), + ), + ); + expect(exchanges.map((r) => r.status).sort()).toEqual([200, 409]); + expect(provider.calls.filter((c) => c.operation === "token")).toHaveLength( + 2, + ); + // A lost store must fail readiness/auth closed rather than create process-local sessions. + await redis.stop(); + expect((await fetchInstance(a.url + "/health/ready")).status).toBe(503); + expect( + (await fetchInstance(b.url + "/user/info", { headers: { cookie } })) + .status, + ).toBe(503); + } finally { + await a.stop(); + await b.stop(); + config.remove(); + await provider.stop(); + await redis.stop(); + } +}); diff --git a/tests/integration/integrity-concurrency.test.ts b/tests/integration/integrity-concurrency.test.ts new file mode 100644 index 0000000..aaa134b --- /dev/null +++ b/tests/integration/integrity-concurrency.test.ts @@ -0,0 +1,53 @@ +import { test, expect } from "vitest"; +import { startRedis } from "../helpers/redis"; +import { closeRedis } from "@/server/store/redis"; +import { createPlayIntegrityAttemptService } from "@/server/integrity/attempts"; +test("shared attempts and transactions are claimed once with fixed TTL", async () => { + const redis = await startRedis(); + process.env.REDIS_URL = redis.url; + process.env.SHOWCASE_KEY_PREFIX = "integrity-test:"; + const options = { + stateSecret: "fixture-state-secret", + stateIssuer: "fixture-issuer", + stateAudience: "fixture-audience", + }; + try { + const a = createPlayIntegrityAttemptService(options), + b = createPlayIntegrityAttemptService(options); + const attempt = await a.createAttempt({ + phoneNumber: "+12025550123", + clientId: "c", + serverId: "stage", + resolveClient: () => ({ + id: "c", + client_id: "c", + client_secret: "fixture-secret", + redirectUri: "demo://callback", + }), + resolveServer: () => ({ + id: "stage", + url: "http://provider.invalid", + realm: "demo", + }), + }); + expect(attempt).not.toBeNull(); + const claims = await Promise.all([ + a.claimAttempt(attempt!.attemptId), + b.claimAttempt(attempt!.attemptId), + ]); + expect(claims.filter(Boolean)).toHaveLength(1); + const tx = await a.approveAttempt(attempt!.attemptId); + expect(tx).not.toBeNull(); + const state = a.createSignedState(tx!); + const transactions = await Promise.all([ + a.claimTransaction(state), + b.claimTransaction(state), + ]); + expect(transactions.filter((t) => t.status === "claimed")).toHaveLength(1); + expect((await b.claimTransaction(state)).status).toBe("unavailable"); + expect((await a.claimTransaction("forged")).status).toBe("invalid"); + } finally { + await closeRedis(); + await redis.stop(); + } +}); diff --git a/tests/integration/production-runtime.test.ts b/tests/integration/production-runtime.test.ts new file mode 100644 index 0000000..d1f14e6 --- /dev/null +++ b/tests/integration/production-runtime.test.ts @@ -0,0 +1,104 @@ +import { test, expect } from "vitest"; +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +import { randomUUID } from "node:crypto"; +import { chmodSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { freePort } from "../helpers/processes"; +import { configFixture } from "../helpers/config"; +const command = promisify(execFile); +test.runIf(!!process.env.TEST_DOCKER_IMAGE)( + "Docker runs as non-root with runtime config, supports recreate and has no baked private JSON", + async () => { + const id = "showcase-smoke-" + randomUUID().slice(0, 8), + network = id + "-net", + redis = id + "-redis", + app = id + "-app"; + const config = configFixture(); + const filename = path.join(config.dir, "default.json"); + chmodSync(filename, 0o644); + const port = await freePort(); + const docker = (...args: string[]) => + command("docker", args, { timeout: 120000 }); + const args = [ + "run", + "--rm", + "-d", + "--network", + network, + "--name", + app, + "-p", + `127.0.0.1:${port}:3002`, + "-e", + "NODE_ENV=production", + "-e", + "NODE_CONFIG_DIR=/run/showcase-config", + "-e", + `REDIS_URL=redis://${redis}:6379`, + "--mount", + `type=bind,source=${filename},target=/run/showcase-config/default.json,readonly`, + process.env.TEST_DOCKER_IMAGE!, + ]; + async function ready() { + for (let i = 0; i < 120; i++) { + try { + const r = await fetch(`http://127.0.0.1:${port}/health/ready`); + if (r.ok) return; + } catch {} + await new Promise((r) => setTimeout(r, 250)); + } + throw new Error((await docker("logs", app)).stdout); + } + try { + await docker("network", "create", network); + await docker( + "run", + "--rm", + "-d", + "--network", + network, + "--name", + redis, + "redis:7.4.7-alpine", + ); + await docker(...args); + await ready(); + expect((await docker("exec", app, "id", "-u")).stdout.trim()).not.toBe( + "0", + ); + expect( + ( + await docker( + "exec", + app, + "node", + "-e", + "console.log(require('node:fs').existsSync('/usr/app/config/default.json'))", + ) + ).stdout.trim(), + ).toBe("false"); + const response = await fetch(`http://127.0.0.1:${port}/api/config`); + const publicConfig = await response.json(); + expect(publicConfig.realm).toBe("demo"); + expect(JSON.stringify(publicConfig)).not.toContain("fixture-secret"); + await docker("stop", "-t", "10", app); + writeFileSync( + filename, + JSON.stringify({ ...config.data, realm: "changed-runtime" }), + ); + await docker(...args); + await ready(); + expect( + (await (await fetch(`http://127.0.0.1:${port}/api/config`)).json()) + .realm, + ).toBe("changed-runtime"); + } finally { + await docker("rm", "-f", app).catch(() => {}); + await docker("rm", "-f", redis).catch(() => {}); + await docker("network", "rm", network).catch(() => {}); + config.remove(); + } + }, + 120000, +); diff --git a/tests/integration/rate-limit.test.ts b/tests/integration/rate-limit.test.ts new file mode 100644 index 0000000..87cd179 --- /dev/null +++ b/tests/integration/rate-limit.test.ts @@ -0,0 +1,26 @@ +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/tests/integration/sessions.test.ts b/tests/integration/sessions.test.ts new file mode 100644 index 0000000..7776249 --- /dev/null +++ b/tests/integration/sessions.test.ts @@ -0,0 +1,56 @@ +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 redis.client.pExpire("session-test:session:" + next, 100); + await readSession(next); + expect( + await redis.client.pTTL("session-test:session:" + next), + ).toBeLessThanOrEqual(100); + await new Promise((resolve) => setTimeout(resolve, 150)); + expect(await readSession(next)).toBeNull(); + await deleteSession(next); + expect(await readSession(next)).toBeNull(); + expect(await readSession("invalid")).toBeNull(); + } finally { + await closeRedis(); + await redis.stop(); + } +}); diff --git a/tests/services/auth.test.ts b/tests/services/auth.test.ts new file mode 100644 index 0000000..6c58d81 --- /dev/null +++ b/tests/services/auth.test.ts @@ -0,0 +1,351 @@ +import { expect, test, vi } from "vitest"; +import { createAuthService } from "@/server/auth/service"; +import { serviceContext as context } from "../helpers/service-context"; +import { decodeStart, decodeCallback, decodeMobile } from "@/server/auth/input"; +function setup() { + const calls: string[] = []; + const results = { + saveResult: vi.fn(async () => { + calls.push("save"); + }), + waitForResult: vi.fn(async () => undefined), + getUserInfo: vi.fn(async () => undefined), + }; + const sessions = { + read: vi.fn(async () => null), + create: vi.fn(async () => { + calls.push("session"); + return "new-session"; + }), + }; + const provider = { + buildAuthorizationUrl: vi.fn(() => "https://provider.example/auth"), + exchangeAuthorizationCode: vi.fn(async () => ({ + tokenInfo: { access_token: "token" }, + userInfo: { phone_number_verified: false }, + })), + }; + const record = { + state: "s-qrcode", + userFlow: "login_ip", + serverId: "stage", + clientId: "client", + origin: context.origin, + qr: true, + ownerHash: "hash", + status: "processing" as const, + expiresAt: Date.now() + 300000, + }; + const attempts = { + create: vi.fn(async () => {}), + start: vi.fn(async () => ({ ...record, state: "s", phone: "+123" })), + claimCallback: vi.fn(async () => record), + finishCallback: vi.fn(async () => {}), + readOwned: vi.fn(async () => record), + claimCompletion: vi.fn(async () => record), + finishCompletion: vi.fn(async () => {}), + }; + return { + calls, + results, + sessions, + provider, + attempts, + service: createAuthService({ + results, + attempts, + sessions, + provider, + id: () => "generated-state", + }), + }; +} +test("authorization validates target before claiming and uses the stored phone/state", async () => { + const { service, provider, attempts } = setup(); + const binding = { owner: "owner", callback: "callback" }; + await expect( + service.startWebAuthentication( + context, + decodeStart({ server_id: "missing" }), + binding, + ), + ).rejects.toThrow("SERVER_NOT_FOUND"); + await expect( + service.startWebAuthentication( + context, + decodeStart({ user_flow: "missing" }), + binding, + ), + ).rejects.toThrow("CLIENT_NOT_FOUND"); + expect(attempts.start).not.toHaveBeenCalled(); + await service.startWebAuthentication( + context, + decodeStart({ user_flow: "login_ip", state: "s", phone: "tampered" }), + binding, + ); + expect(provider.buildAuthorizationUrl).toHaveBeenCalledWith( + expect.objectContaining({ state: "s", phone: "+123", appEnv: "stage" }), + ); +}); +test("callback persists both results without notification, and boolean false remains success", async () => { + const s = setup(); + const result = await s.service.handleAuthorizationCallback(context, { + ...decodeCallback({ code: "c", state: "s-qrcode" }), + userFlow: "login_ip", + serverId: "stage", + previousSessionId: "", + backchannel: false, + callbackBinding: "callback", + }); + expect(s.calls).toEqual(["save"]); + expect(result).toEqual({ kind: "qr", failed: false }); +}); +test("a bound IM browser return reuses a backchannel result without another exchange", async () => { + const s = setup(); + s.attempts.claimCallback.mockResolvedValueOnce({ + ...(await s.attempts.claimCallback()), + status: "completed", + } as never); + expect( + await s.service.handleAuthorizationCallback(context, { + ...decodeCallback({ code: "c", state: "s-qrcode" }), + userFlow: "login_ip", + serverId: "stage", + previousSessionId: "", + backchannel: false, + callbackBinding: "callback", + }), + ).toEqual({ + kind: "redirect", + url: context.origin + "/auth/complete?state=s-qrcode", + }); + expect(s.provider.exchangeAuthorizationCode).not.toHaveBeenCalled(); +}); +test("bound provider cancellation adopts the existing session without provider calls", async () => { + const s = setup(); + await s.service.handleAuthorizationCallback(context, { + ...decodeCallback({ error: "access_denied" }), + userFlow: "unknown", + serverId: "missing", + previousSessionId: "old", + backchannel: false, + callbackBinding: "callback", + }); + expect(s.sessions.create).toHaveBeenCalledWith( + { isAuthenticated: false, errorMessage: "access_denied" }, + "old", + ); + expect(s.provider.exchangeAuthorizationCode).not.toHaveBeenCalled(); +}); +test("store failures propagate; mobile passes submitted redirect and S2S stays a repository read", async () => { + const s = setup(); + s.results.saveResult.mockRejectedValueOnce(new Error("offline")); + await expect( + s.service.handleAuthorizationCallback(context, { + ...decodeCallback({ code: "c", state: "s-qrcode" }), + userFlow: "login_ip", + serverId: "stage", + previousSessionId: "", + backchannel: false, + callbackBinding: "callback", + }), + ).rejects.toThrow("offline"); + await s.service.exchangeMobileCode( + context, + decodeMobile({ + client_id: "client", + redirect_uri: "demo://callback", + code: "c", + }), + ); + expect(s.provider.exchangeAuthorizationCode).toHaveBeenLastCalledWith( + expect.objectContaining({ redirectUri: "demo://callback" }), + ); + await s.service.readS2sResult("s"); + await s.service.readS2sResult("s"); + expect(s.results.getUserInfo).toHaveBeenCalledTimes(2); +}); + +test("completion creates a replacement authenticated session only after a stored result is found", async () => { + const s = setup(); + await expect( + s.service.completeWebAuthentication(context, "missing", "old", "owner"), + ).rejects.toThrow("AUTH_STATE_INVALID"); + expect(s.sessions.create).not.toHaveBeenCalled(); + const result = { + userInfo: { sub: "user" }, + client_id: "client", + client_title: "Login", + state: "s", + }; + const service = createAuthService({ + attempts: s.attempts, + sessions: s.sessions, + results: { ...s.results, waitForResult: async () => result }, + }); + expect( + await service.completeWebAuthentication(context, "s", "old", "owner"), + ).toBe("new-session"); + expect(s.sessions.create).toHaveBeenCalledWith( + { isAuthenticated: true, userData: result }, + "old", + ); +}); + +test("provider error keeps prior authenticated data and displays the error code", async () => { + const s = setup(); + const previous = { + isAuthenticated: true, + userData: { + userInfo: { sub: "existing-user" }, + client_id: "client", + client_title: "Login", + state: "old", + }, + }; + const service = createAuthService({ + attempts: s.attempts, + sessions: { ...s.sessions, read: async () => previous }, + provider: s.provider, + }); + await service.handleAuthorizationCallback(context, { + ...decodeCallback({ + error: "unknown-error", + error_description: "PRIVATE_PROVIDER_TEXT", + }), + userFlow: "unknown", + serverId: "missing", + previousSessionId: "old", + backchannel: false, + callbackBinding: "callback", + }); + expect(s.sessions.create).toHaveBeenCalledWith( + { ...previous, errorMessage: "unknown-error" }, + "old", + ); + expect(s.provider.exchangeAuthorizationCode).not.toHaveBeenCalled(); +}); + +test("an invalid callback claim is rejected before provider calls, persistence, or sessions", async () => { + const s = setup(); + s.attempts.claimCallback.mockRejectedValueOnce( + new Error("AUTH_STATE_INVALID"), + ); + await expect( + s.service.handleAuthorizationCallback(context, { + ...decodeCallback({ code: "c", state: ["one", "two"] }), + userFlow: "login_ip", + serverId: "stage", + previousSessionId: "", + backchannel: false, + callbackBinding: "callback", + }), + ).rejects.toThrow("AUTH_STATE_INVALID"); + expect(s.provider.exchangeAuthorizationCode).not.toHaveBeenCalled(); + expect(s.results.saveResult).not.toHaveBeenCalled(); + expect(s.sessions.create).not.toHaveBeenCalled(); +}); + +test("finished status is restored only for an authenticated session with the exact valid state", async () => { + const s = setup(); + const state = "12345678-1234-1234-1234-123456789abc"; + let authenticated = true; + const service = createAuthService({ + attempts: { + ...s.attempts, + readOwned: vi.fn(async () => { + throw new Error("owner required"); + }), + }, + sessions: { + ...s.sessions, + read: async () => ({ + isAuthenticated: authenticated, + userData: { + state, + userInfo: { sub: "user" }, + client_id: "client", + client_title: "Login", + }, + }), + }, + }); + expect( + await service.getAuthenticationStatus(context, state, "", "session"), + ).toMatchObject({ status: "finished" }); + await expect( + service.getAuthenticationStatus( + context, + "aaaaaaaa-1234-1234-1234-123456789abc", + "", + "session", + ), + ).rejects.toThrow("owner required"); + authenticated = false; + await expect( + service.getAuthenticationStatus(context, state, "", "session"), + ).rejects.toThrow("owner required"); + expect(s.sessions.create).not.toHaveBeenCalled(); +}); + +test("callback error takes priority over code and description; description is used if error is absent", async () => { + for (const [params, expected] of [ + [ + { + error: "msisdn_country_mismatch", + error_description: "Country mismatch", + code: "ignored", + }, + "msisdn_country_mismatch", + ], + [ + { error_description: "Carrier could not verify this number" }, + "Carrier could not verify this number", + ], + [{}, "Unable to complete authentication."], + ] as const) { + const s = setup(); + await s.service.handleAuthorizationCallback(context, { + ...decodeCallback({ ...params, state: "s-qrcode" }), + userFlow: "login_ip", + serverId: "stage", + previousSessionId: "", + backchannel: false, + callbackBinding: "callback", + }); + expect(s.sessions.create).toHaveBeenCalledWith( + expect.objectContaining({ errorMessage: expected }), + "", + ); + expect(s.attempts.finishCallback).toHaveBeenCalledWith( + "s-qrcode", + expect.objectContaining({ callbackMessage: expected }), + ); + expect(s.provider.exchangeAuthorizationCode).not.toHaveBeenCalled(); + } +}); + +test("QR owner can consume a bound provider callback error into their own session", async () => { + const s = setup(); + const attempt = await s.attempts.claimCompletion(); + s.attempts.claimCompletion.mockResolvedValueOnce({ + ...attempt, + failure: { + reason: "AUTH_CANCELLED", + stage: "application", + callbackMessage: "msisdn_country_mismatch", + }, + } as never); + const result = await s.service.completeWebAuthentication( + context, + "s-qrcode", + "old", + "owner", + ); + expect(result).toEqual({ sessionId: "new-session", callbackError: true }); + expect(s.sessions.create).toHaveBeenCalledWith( + { isAuthenticated: false, errorMessage: "msisdn_country_mismatch" }, + "old", + ); + expect(s.attempts.finishCompletion).toHaveBeenCalledWith("s-qrcode"); +}); diff --git a/tests/services/ciba.test.ts b/tests/services/ciba.test.ts new file mode 100644 index 0000000..fef0144 --- /dev/null +++ b/tests/services/ciba.test.ts @@ -0,0 +1,136 @@ +import { expect, test, vi } from "vitest"; +import { createCibaService } from "@/server/ciba/service"; +import { decodeCiba } from "@/server/ciba/input"; +import { serviceContext as context } from "../helpers/service-context"; +function setup() { + const sessions = { + read: vi.fn(async () => null), + create: vi.fn(async () => "new-session"), + }; + const provider = { + sendSmsCode: vi.fn(async () => ({ authReqId: "auth", nonce: "nonce" })), + createSimCredentialRequest: vi.fn(async () => ({ + authReqId: "auth", + nonce: "nonce", + })), + verifySmsCode: vi.fn(async () => ({ sub: "user" })), + exchangeSimCredential: vi.fn(async () => ({ sub: "user" })), + }; + return { + sessions, + provider, + service: createCibaService({ sessions, provider }), + }; +} +test("start resolves credentials, preserves requested scope, and classifies provider failure without HTTP", async () => { + const s = setup(); + const result = await s.service.sendSmsCode( + context, + decodeCiba({ client_id: "client", scope: ["openid", "phone"] }), + ); + expect(result.kind).toBe("started"); + expect(s.provider.sendSmsCode).toHaveBeenCalledWith( + expect.objectContaining({ + clientSecret: "secret", + reqScope: ["openid", "phone"], + }), + ); + s.provider.createSimCredentialRequest.mockRejectedValueOnce({ + message: "upstream failed", + response: { status: 503, data: { error: "offline" } }, + }); + const failure = await s.service.createSimCredentialRequest( + context, + decodeCiba({ client_id: "client" }), + ); + expect(failure).toMatchObject({ + kind: "failure", + failure: { + message: "upstream failed", + status: 503, + data: { error: "offline" }, + }, + }); +}); +test("successful token stores display data before returning the new session", async () => { + const s = setup(); + const outcome = await s.service.exchangeSimCredential( + context, + decodeCiba({ + client_id: "client", + nonce: "n", + vp_token: { value: "opaque" }, + }), + "old", + ); + expect(outcome).toMatchObject({ + kind: "token_exchanged", + sessionId: "new-session", + data: { sub: "user" }, + }); + expect(s.sessions.create).toHaveBeenCalledWith( + { + isAuthenticated: true, + userData: { + userInfo: { sub: "user" }, + client_id: "client", + client_title: "SIM", + state: "n", + }, + }, + "old", + ); +}); +test("token failure still adopts an authenticated demo error session", async () => { + const s = setup(); + s.provider.verifySmsCode.mockRejectedValueOnce({ + message: "invalid code", + response: { status: 401, data: { error: "invalid_grant" } }, + }); + const outcome = await s.service.verifySmsCode( + context, + decodeCiba({ client_id: "client", nonce: "n" }), + "old", + ); + expect(outcome).toMatchObject({ + kind: "failure", + sessionId: "new-session", + failure: { status: 401 }, + }); + expect(s.sessions.create).toHaveBeenCalledWith( + expect.objectContaining({ + isAuthenticated: true, + userData: expect.objectContaining({ + userInfo: { + error: "invalid code", + status: 401, + data: { error: "invalid_grant" }, + }, + }), + }), + "old", + ); +}); +test("a successful token followed by a session failure attempts one error-display adoption; repeated store failure propagates", async () => { + const s = setup(); + s.sessions.create.mockRejectedValueOnce(new Error("store failed")); + expect( + await s.service.verifySmsCode( + context, + decodeCiba({ client_id: "client" }), + "old", + ), + ).toMatchObject({ + kind: "failure", + failure: { message: "store failed", status: 500 }, + }); + expect(s.sessions.create).toHaveBeenCalledTimes(2); + s.sessions.create.mockRejectedValue(new Error("offline")); + await expect( + s.service.verifySmsCode( + context, + decodeCiba({ client_id: "client" }), + "old", + ), + ).rejects.toThrow("offline"); +}); diff --git a/tests/services/integrity.test.ts b/tests/services/integrity.test.ts new file mode 100644 index 0000000..a7ea292 --- /dev/null +++ b/tests/services/integrity.test.ts @@ -0,0 +1,313 @@ +import { test, expect, vi } from "vitest"; +import { createIntegrityService } from "@/server/integrity/service"; +import { UpstreamUnavailableError } from "@/server/shared/errors"; +import { serviceContext } from "../helpers/service-context"; +function setup() { + const attempt = { + id: "attempt", + requestHash: "hash", + client: { + client_id: "c", + client_secret: "s", + redirectUri: "demo://callback", + }, + server: { url: "https://provider.example/", realm: "a b" }, + }; + const transaction = { + id: "attempt--tx", + expiresAt: "future", + status: "approved", + attempt, + }; + const attempts = { + createAttempt: vi.fn(async () => ({ + attemptId: "attempt", + requestHash: "hash", + expiresAt: "future", + })), + claimAttempt: vi.fn(async () => attempt), + approveAttempt: vi.fn(async () => transaction), + rejectAttempt: vi.fn(async () => null), + createSignedState: vi.fn(() => "a.b.c"), + claimTransaction: vi.fn(async () => ({ status: "claimed", transaction })), + completeTransaction: vi.fn(async () => null), + }; + const verify = vi.fn(async () => ({ + decision: "allow", + reasonCodes: [] as string[], + })); + const exchange = vi.fn(async () => ({ userInfo: { sub: "user" } })); + const context = { + ...serviceContext, + config: { + ...serviceContext.config, + play_integrity_verify_domains: [" VERIFY.EXAMPLE "], + }, + hostname: "verify.example", + }; + return { + attempts, + verify, + exchange, + context, + service: createIntegrityService({ + attempts, + verify, + exchange, + policy: { userFlow: "login_ip", packageName: "com.fixture" }, + }), + }; +} +test("only configured verification hostname invokes verifier and preserves signed-state output", async () => { + const s = setup(); + expect( + await s.service.execute("verify", s.context, { + attempt_id: "attempt", + integrity_token: "opaque", + }), + ).toMatchObject({ kind: "verified", state: "a.b.c" }); + expect(s.verify).toHaveBeenCalledWith({ + integrityToken: "opaque", + expectedRequestHash: "hash", + expectedPackageName: "com.fixture", + }); + s.verify.mockClear(); + await s.service.execute( + "verify", + { ...s.context, hostname: "bypass.example" }, + { attempt_id: "attempt", integrity_token: "opaque" }, + ); + expect(s.verify).not.toHaveBeenCalled(); +}); +test("policy denial rejects the attempt, while verifier outages retain their reason", async () => { + const s = setup(); + s.verify.mockResolvedValueOnce({ + decision: "deny", + reasonCodes: ["PACKAGE_NAME_MISMATCH"], + }); + expect( + await s.service.execute("verify", s.context, { + attempt_id: "attempt", + integrity_token: "t", + }), + ).toMatchObject({ + kind: "denied", + category: "policy", + reasonCodes: ["PACKAGE_NAME_MISMATCH"], + }); + expect(s.attempts.rejectAttempt).toHaveBeenCalledWith( + "attempt", + "PACKAGE_NAME_MISMATCH", + ); + s.verify.mockRejectedValueOnce( + new UpstreamUnavailableError("GOOGLE_TIMEOUT", new Error("private")), + ); + expect( + await s.service.execute("verify", s.context, { + attempt_id: "attempt", + integrity_token: "t", + }), + ).toMatchObject({ + category: "unavailable", + decision: "unavailable", + reasonCodes: ["GOOGLE_TIMEOUT"], + }); +}); +test("exchange uses captured credentials/redirect and finalizes before returning; failure finalizes as failed", async () => { + const s = setup(); + expect( + await s.service.execute("token-exchange", s.context, { + code: "c", + state: "a.b.c", + }), + ).toEqual({ kind: "exchanged", userInfo: { sub: "user" } }); + expect(s.exchange).toHaveBeenCalledWith( + "https://provider.example/realms/a%20b/protocol/openid-connect/token", + "https://provider.example/realms/a%20b/protocol/openid-connect/userinfo", + expect.objectContaining({ + redirect_uri: "demo://callback", + client_secret: "s", + }), + ); + expect(s.attempts.completeTransaction).toHaveBeenCalledWith( + "attempt--tx", + "consumed", + ); + s.exchange.mockRejectedValueOnce(new Error("401")); + expect( + await s.service.execute("token-exchange", s.context, { + code: "c", + state: "a.b.c", + }), + ).toMatchObject({ + category: "exchange", + reasonCodes: ["IPIFICATION_EXCHANGE_FAILED"], + }); + expect(s.attempts.completeTransaction).toHaveBeenLastCalledWith( + "attempt--tx", + "failed", + ); +}); +test("validation/replay misses do not call the provider; repository failures are service unavailable", async () => { + const s = setup(); + expect( + await s.service.execute("verify", s.context, { + attempt_id: ["a"], + integrity_token: "t", + }), + ).toMatchObject({ category: "input" }); + expect(s.attempts.claimAttempt).not.toHaveBeenCalled(); + const service = createIntegrityService({ + attempts: { + ...s.attempts, + claimTransaction: async () => ({ status: "unavailable" }), + }, + verify: s.verify, + exchange: s.exchange, + policy: {}, + }); + expect( + await service.execute("token-exchange", s.context, { + code: "c", + state: "a.b.c", + }), + ).toMatchObject({ + category: "conflict", + reasonCodes: ["TRANSACTION_UNAVAILABLE"], + }); + expect(s.exchange).not.toHaveBeenCalled(); + s.attempts.claimAttempt.mockRejectedValueOnce(new Error("Redis offline")); + expect( + await s.service.execute("verify", s.context, { + attempt_id: "a", + integrity_token: "t", + }), + ).toMatchObject({ + category: "unavailable", + reasonCodes: ["SERVICE_UNAVAILABLE"], + }); +}); + +test("attempt creation resolves a configured flow/server and delegates the immutable credential snapshot", async () => { + const s = setup(); + expect( + await s.service.execute("attempt", s.context, { + phone_number: "+12025550123", + server_id: "stage", + }), + ).toEqual({ + kind: "created", + attemptId: "attempt", + requestHash: "hash", + expiresAt: "future", + }); + expect(s.attempts.createAttempt).toHaveBeenCalledWith( + expect.objectContaining({ + clientId: "client", + serverId: "stage", + resolveClient: expect.any(Function), + resolveServer: expect.any(Function), + }), + ); + expect( + await s.service.execute("attempt", s.context, { + phone_number: "+12025550123", + server_id: "missing", + }), + ).toMatchObject({ category: "input", reasonCodes: ["INVALID_REQUEST"] }); +}); + +test("approval failure rejects the claimed attempt; rejection-store failures keep the outer unavailable boundary", async () => { + const s = setup(); + s.attempts.approveAttempt.mockRejectedValueOnce( + new Error("store unavailable"), + ); + expect( + await s.service.execute("verify", s.context, { + attempt_id: "a", + integrity_token: "t", + }), + ).toMatchObject({ + category: "unavailable", + reasonCodes: ["GOOGLE_VERIFICATION_FAILED"], + }); + expect(s.attempts.rejectAttempt).toHaveBeenCalledWith( + "attempt", + "GOOGLE_VERIFICATION_FAILED", + ); + s.verify.mockResolvedValueOnce({ + decision: "deny", + reasonCodes: ["POLICY_DENIED"], + }); + s.attempts.rejectAttempt.mockRejectedValue(new Error("store unavailable")); + expect( + await s.service.execute("verify", s.context, { + attempt_id: "a", + integrity_token: "t", + }), + ).toMatchObject({ + category: "unavailable", + reasonCodes: ["SERVICE_UNAVAILABLE"], + }); +}); + +test("failed consumed-finalization attempts failed-finalization before returning; a failed fallback remains unavailable", async () => { + const s = setup(); + s.attempts.completeTransaction.mockRejectedValueOnce( + new Error("store unavailable"), + ); + expect( + await s.service.execute("token-exchange", s.context, { + code: "c", + state: "a.b.c", + }), + ).toMatchObject({ + category: "exchange", + reasonCodes: ["IPIFICATION_EXCHANGE_FAILED"], + }); + expect(s.attempts.completeTransaction.mock.calls).toEqual([ + ["attempt--tx", "consumed"], + ["attempt--tx", "failed"], + ]); + s.attempts.completeTransaction.mockRejectedValue( + new Error("store unavailable"), + ); + expect( + await s.service.execute("token-exchange", s.context, { + code: "c", + state: "a.b.c", + }), + ).toMatchObject({ + category: "unavailable", + reasonCodes: ["SERVICE_UNAVAILABLE"], + }); +}); + +test("creation failure stays input-classified while transaction claim failure stays unavailable without provider calls", async () => { + const s = setup(); + s.attempts.createAttempt.mockRejectedValueOnce( + new Error("store unavailable"), + ); + expect( + await s.service.execute("attempt", s.context, { + phone_number: "+12025550123", + server_id: "stage", + }), + ).toMatchObject({ + category: "input", + reasonCodes: ["INVALID_REQUEST"], + }); + s.attempts.claimTransaction.mockRejectedValueOnce( + new Error("store unavailable"), + ); + expect( + await s.service.execute("token-exchange", s.context, { + code: "c", + state: "a.b.c", + }), + ).toMatchObject({ + category: "unavailable", + reasonCodes: ["SERVICE_UNAVAILABLE"], + }); + expect(s.exchange).not.toHaveBeenCalled(); +}); diff --git a/tests/unit/auth-failure.test.ts b/tests/unit/auth-failure.test.ts new file mode 100644 index 0000000..78963b2 --- /dev/null +++ b/tests/unit/auth-failure.test.ts @@ -0,0 +1,30 @@ +import { test, expect } from "vitest"; +import { AxiosError } from "axios"; +import { describeAuthFailure } from "@/server/auth/failure"; +import { authFailureReason, safeProviderCode } from "@/shared/auth-failures"; +test.each(["ECONNREFUSED", "ETIMEDOUT"])( + "%s offers temporary provider recovery without inventing a credential diagnosis", + (code) => { + expect( + describeAuthFailure(new AxiosError("sensitive URL/code", code)), + ).toEqual({ + reason: "PROVIDER_UNAVAILABLE", + stage: "token", + status: undefined, + providerCode: undefined, + }); + }, +); +test("application failure does not expose raw exception text", () => { + expect(describeAuthFailure(new Error("redis://user:secret@host"))).toEqual({ + reason: "AUTH_FAILED", + stage: "application", + status: undefined, + providerCode: undefined, + }); +}); +test("unknown provider data and inherited object keys cannot become public error codes", () => { + expect(safeProviderCode("secret_token_value")).toBeUndefined(); + expect(authFailureReason("__proto__")).toBe("AUTH_FAILED"); + expect(authFailureReason(["CLIENT_AUTH_FAILED"])).toBe("AUTH_FAILED"); +}); diff --git a/tests/unit/auth-results.test.ts b/tests/unit/auth-results.test.ts new file mode 100644 index 0000000..7eef6f9 --- /dev/null +++ b/tests/unit/auth-results.test.ts @@ -0,0 +1,30 @@ +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/tests/unit/auth-status.test.ts b/tests/unit/auth-status.test.ts new file mode 100644 index 0000000..ad31199 --- /dev/null +++ b/tests/unit/auth-status.test.ts @@ -0,0 +1,170 @@ +import { test, expect, vi, afterEach } from "vitest"; +import { + watchAuthStatus, + completionUrlForAttempt, + type AuthStatusUpdate, +} from "@/features/auth/auth-status"; +const state = "12345678-1234-1234-1234-123456789abc"; +const origin = "https://showcase.example"; +const pending = { status: "pending", started: false, expiresAt: 120000 }; +const reply = (body: unknown, status = 200) => + new Response(JSON.stringify(body), { status }); +afterEach(() => { + vi.useRealTimers(); +}); +function setup(fetcher: typeof fetch, extra = {}) { + vi.useFakeTimers(); + vi.setSystemTime(0); + const events: AuthStatusUpdate[] = []; + const watcher = watchAuthStatus({ + state, + origin, + fetcher, + maxExpiresAt: 420000, + onUpdate: (event) => events.push(event), + ...extra, + }); + return { watcher, events }; +} +test("completion URLs require the exact origin, path and single state", () => { + expect( + completionUrlForAttempt(`/auth/complete?state=${state}`, origin, state), + ).toBe(`${origin}/auth/complete?state=${state}`); + for (const value of [ + `https://evil.example/auth/complete?state=${state}`, + `/auth/complete?state=wrong`, + `/auth/complete?state=${state}&state=other`, + `/other?state=${state}`, + `https://user@showcase.example/auth/complete?state=${state}`, + ]) + expect(completionUrlForAttempt(value, origin, state)).toBeUndefined(); +}); +test("checks never overlap and cleanup ignores a late response and clears all timers", async () => { + let resolve!: (response: Response) => void; + const fetcher = vi.fn( + () => + new Promise((r) => { + resolve = r; + }), + ); + const { watcher, events } = setup(fetcher as typeof fetch); + void watcher.check(); + void watcher.check(); + expect(fetcher).toHaveBeenCalledTimes(1); + watcher.stop(); + expect(vi.getTimerCount()).toBe(0); + resolve( + reply({ + status: "completed", + completionUrl: `/auth/complete?state=${state}`, + }), + ); + await vi.advanceTimersByTimeAsync(0); + expect(events).toEqual([]); +}); +test("a hung check times out and a later check recovers", async () => { + const fetcher = vi + .fn() + .mockImplementationOnce( + (_url, options) => + new Promise((_resolve, reject) => + options.signal.addEventListener("abort", () => + reject(new DOMException("Aborted", "AbortError")), + ), + ), + ) + .mockResolvedValue(reply(pending)); + const { watcher, events } = setup(fetcher as typeof fetch, { + requestTimeoutMs: 20, + intervalMs: 30, + }); + await vi.advanceTimersByTimeAsync(20); + expect(events.at(-1)).toEqual({ kind: "unavailable" }); + await vi.advanceTimersByTimeAsync(10); + expect(events.at(-1)).toMatchObject({ kind: "pending", started: false }); + watcher.stop(); +}); +test("finished is terminal and never attempts a second completion", async () => { + const fetcher = vi.fn().mockResolvedValue(reply({ status: "finished" })); + const { watcher, events } = setup(fetcher as typeof fetch); + await vi.advanceTimersByTimeAsync(10000); + await watcher.check(); + expect(events).toEqual([{ kind: "finished" }]); + expect(fetcher).toHaveBeenCalledTimes(1); + expect(vi.getTimerCount()).toBe(0); +}); +test("malformed or mismatched completion responses cannot navigate", async () => { + const fetcher = vi.fn().mockResolvedValue( + reply({ + status: "completed", + completionUrl: "https://evil.example/auth/complete?state=" + state, + }), + ); + const { watcher, events } = setup(fetcher as typeof fetch); + await vi.advanceTimersByTimeAsync(0); + expect(events).toEqual([{ kind: "unavailable" }]); + watcher.stop(); +}); +test("a permanent outage terminates at the hard deadline", async () => { + const fetcher = vi.fn().mockRejectedValue(new TypeError("Offline")); + const { events } = setup(fetcher as typeof fetch, { + maxExpiresAt: 100, + intervalMs: 20, + }); + await vi.advanceTimersByTimeAsync(100); + expect(events.at(-1)).toEqual({ + kind: "expired", + reason: "AUTH_STATE_EXPIRED", + }); + expect(vi.getTimerCount()).toBe(0); +}); +test("a QR scanned just before scan expiry keeps its separate completion window", async () => { + let scanned = false; + const fetcher = vi.fn(async () => + reply({ + status: "pending", + started: scanned, + expiresAt: scanned ? 419000 : 120000, + }), + ); + const { watcher, events } = setup(fetcher as typeof fetch, { + intervalMs: 3000, + }); + await vi.advanceTimersByTimeAsync(0); + vi.setSystemTime(119000); + scanned = true; + await watcher.check(); + await vi.advanceTimersByTimeAsync(2000); + expect(events.at(-1)).toMatchObject({ + kind: "pending", + started: true, + expiresAt: 419000, + }); + expect(events.some((event) => event.kind === "expired")).toBe(false); + watcher.stop(); +}); + +test("polls immediately and every two seconds, then stops after completion", async () => { + const fetcher = vi.fn().mockImplementation(async () => reply(pending)); + const { events } = setup(fetcher as typeof fetch); + await vi.advanceTimersByTimeAsync(0); + expect(fetcher).toHaveBeenCalledTimes(1); + await vi.advanceTimersByTimeAsync(1999); + expect(fetcher).toHaveBeenCalledTimes(1); + await vi.advanceTimersByTimeAsync(1); + expect(fetcher).toHaveBeenCalledTimes(2); + fetcher.mockImplementation(async () => + reply({ + status: "completed", + completionUrl: `/auth/complete?state=${state}`, + }), + ); + await vi.advanceTimersByTimeAsync(2000); + expect(events.at(-1)).toEqual({ + kind: "complete", + url: `${origin}/auth/complete?state=${state}`, + }); + await vi.advanceTimersByTimeAsync(10000); + expect(fetcher).toHaveBeenCalledTimes(3); + expect(vi.getTimerCount()).toBe(0); +}); diff --git a/tests/unit/canonical-json.test.ts b/tests/unit/canonical-json.test.ts new file mode 100644 index 0000000..a126679 --- /dev/null +++ b/tests/unit/canonical-json.test.ts @@ -0,0 +1,14 @@ +import { test, expect } from "vitest"; +import { + canonicalJson, + createRequestHash, +} from "@/server/shared/canonical-json"; +test("canonical payload rejects ambiguous inputs and hashes sorted nested JSON", () => { + expect(canonicalJson({ b: 1, a: { z: 2, c: 3 } })).toBe( + '{"a":{"c":3,"z":2},"b":1}', + ); + expect(createRequestHash("A", { b: 1, a: 2 })).toBe( + createRequestHash("A", { a: 2, b: 1 }), + ); + expect(() => canonicalJson({ x: NaN })).toThrow(); +}); diff --git a/tests/unit/config.test.ts b/tests/unit/config.test.ts new file mode 100644 index 0000000..15025ce --- /dev/null +++ b/tests/unit/config.test.ts @@ -0,0 +1,54 @@ +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", + }, + 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/tests/unit/context.test.ts b/tests/unit/context.test.ts new file mode 100644 index 0000000..37608d2 --- /dev/null +++ b/tests/unit/context.test.ts @@ -0,0 +1,54 @@ +import { expect, test } from "vitest"; +import { resolveRequestIdentity } from "@/server/http/context"; +import { readBody } from "@/server/http/body"; +test("public origin and verification hostname come from the trusted incoming request", () => { + 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", + }, + }), + { trustProxyHops: 1 }, + ), + ).toEqual({ + origin: "https://verify.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/tests/unit/integrity-verifier.test.ts b/tests/unit/integrity-verifier.test.ts new file mode 100644 index 0000000..f48da77 --- /dev/null +++ b/tests/unit/integrity-verifier.test.ts @@ -0,0 +1,60 @@ +import { test, expect } from "vitest"; +import { createPlayIntegrityService } from "@/server/integrity/verifier"; +function fixture(overrides: Record = {}) { + const decoded = { + tokenPayloadExternal: { + requestDetails: { + requestHash: "hash", + requestPackageName: "com.fixture", + timestampMillis: "0", + }, + appIntegrity: { appRecognitionVerdict: "PLAY_RECOGNIZED" }, + deviceIntegrity: { deviceRecognitionVerdict: ["MEETS_DEVICE_INTEGRITY"] }, + ...overrides, + }, + }; + return createPlayIntegrityService({ + authClient: { getAccessToken: async () => ({ token: "fixture-token" }) }, + httpClient: { post: async () => ({ data: decoded }) }, + packageName: "com.fixture", + timeoutMs: 10000, + }); +} +test("recognized matching verdict allows without reintroducing timestamp freshness", async () => { + const verdict = await fixture().verify({ + integrityToken: "opaque", + expectedRequestHash: "hash", + expectedPackageName: "com.fixture", + }); + expect(verdict.decision).toBe("allow"); + expect(JSON.stringify(verdict)).not.toContain("fixture-token"); +}); +test("mismatch and device policy fail with stable reason codes", async () => { + const verdict = await fixture({ + deviceIntegrity: { deviceRecognitionVerdict: [] }, + }).verify({ + integrityToken: "opaque", + expectedRequestHash: "wrong", + expectedPackageName: "com.other", + }); + expect(verdict.reasonCodes).toEqual([ + "REQUEST_HASH_MISMATCH", + "PACKAGE_NAME_MISMATCH", + "DEVICE_INTEGRITY_NOT_MET", + ]); +}); +test("decode timeout is mapped to unavailable without exposing Google payload", async () => { + const service = createPlayIntegrityService({ + authClient: { getAccessToken: async () => ({ token: "fixture-token" }) }, + httpClient: { + post: async () => { + throw { code: "ETIMEDOUT" }; + }, + }, + packageName: "com.fixture", + timeoutMs: 10000, + }); + await expect( + service.verify({ integrityToken: "opaque" }), + ).rejects.toMatchObject({ reasonCode: "GOOGLE_DECODE_TIMEOUT" }); +}); diff --git a/tests/unit/provider-error.test.ts b/tests/unit/provider-error.test.ts new file mode 100644 index 0000000..d5b078d --- /dev/null +++ b/tests/unit/provider-error.test.ts @@ -0,0 +1,34 @@ +import { test, expect } from "vitest"; +import { ProviderError, providerFailure } from "@/server/shared/errors"; +import { describeAuthFailure } from "@/server/auth/failure"; +test("provider failures expose one typed envelope with explicit step and preserve endpoint-specific presentation", () => { + const error = new ProviderError("token", { + message: "Request failed with status code 401", + response: { + status: 401, + data: { error: "invalid_client", error_description: "private" }, + }, + }); + expect(providerFailure(error)).toEqual({ + message: "Request failed with status code 401", + status: 401, + data: { error: "invalid_client", error_description: "private" }, + }); + expect(describeAuthFailure(error)).toEqual({ + reason: "CLIENT_AUTH_FAILED", + stage: "token", + status: 401, + providerCode: "invalid_client", + }); + expect( + describeAuthFailure( + new ProviderError("userinfo", { + message: "failed", + response: { status: 401 }, + }), + ), + ).toMatchObject({ reason: "USERINFO_FAILED", stage: "userinfo" }); + expect( + describeAuthFailure(new ProviderError("token", new Error("timeout"))), + ).toMatchObject({ reason: "PROVIDER_UNAVAILABLE", status: undefined }); +}); diff --git a/tests/unit/provider-timeout.test.ts b/tests/unit/provider-timeout.test.ts new file mode 100644 index 0000000..517f035 --- /dev/null +++ b/tests/unit/provider-timeout.test.ts @@ -0,0 +1,80 @@ +import { test, expect, vi } from "vitest"; +import { startProvider } from "../helpers/provider"; +import { exchangeCodeAndGetUserInfo } from "@/server/shared/http-client"; +import { + sendSmsCode, + verifySmsCode, + createSimCredentialRequest, +} from "@/server/ciba/provider"; +import { describeAuthFailure } from "@/server/auth/failure"; +const credentials = { + realm: "demo", + clientId: "fixture-client", + clientSecret: "fixture-secret", +}; +for (const operation of ["token", "userinfo", "auth", "dcql"] as const) { + test(`provider ${operation} is bounded without retries or leaking request data`, async () => { + vi.stubEnv("PROVIDER_HTTP_TIMEOUT_MS", "40"); + const provider = await startProvider(); + provider.behavior.delays[operation] = 180; + try { + const request = + operation === "auth" + ? sendSmsCode({ ...credentials, authServerUrl: provider.url }) + : operation === "dcql" + ? createSimCredentialRequest({ + ...credentials, + authServerUrl: provider.url, + }) + : exchangeCodeAndGetUserInfo( + provider.url + "/token", + provider.url + "/userinfo", + { code: "PRIVATE_CODE", client_secret: "PRIVATE_SECRET" }, + ); + await expect(request).rejects.toMatchObject({ timedOut: true }); + const failure = await request.catch((error) => error); + expect(describeAuthFailure(failure).reason).toBe("PROVIDER_TIMEOUT"); + expect(failure.message).toBe( + "Authentication provider did not respond in time.", + ); + expect(JSON.stringify(describeAuthFailure(failure))).not.toMatch( + /PRIVATE_CODE|PRIVATE_SECRET|fixture-secret/, + ); + expect( + provider.calls.filter((call) => call.operation === operation), + ).toHaveLength(1); + if (operation === "token") + expect( + provider.calls.some((call) => call.operation === "userinfo"), + ).toBe(false); + } finally { + vi.unstubAllEnvs(); + await provider.stop(); + } + }); +} +test("CIBA callback timeout stays best-effort, then token and userinfo run once", async () => { + vi.stubEnv("PROVIDER_HTTP_TIMEOUT_MS", "40"); + const provider = await startProvider(); + provider.behavior.delays.callback = 180; + try { + await expect( + verifySmsCode({ + ...credentials, + authServerUrl: provider.url, + authReqId: "fixture", + smsCode: "123456", + }), + ).resolves.toEqual(provider.behavior.userInfo); + expect(provider.calls.map((call) => call.operation)).toEqual([ + "callback", + "token", + "userinfo", + ]); + // Token/userinfo must finish before the stalled callback responds. + expect(provider.responded).not.toContain("callback"); + } finally { + vi.unstubAllEnvs(); + await provider.stop(); + } +}); diff --git a/tests/unit/public-callbacks.test.ts b/tests/unit/public-callbacks.test.ts new file mode 100644 index 0000000..a2b65af --- /dev/null +++ b/tests/unit/public-callbacks.test.ts @@ -0,0 +1,21 @@ +import { test, expect } from "vitest"; +import { toPublicConfig } from "@/server/config/public"; +import { serviceContext } from "../helpers/service-context"; +test("public callback URLs identify default and alternate servers", () => { + const config = { + ...serviceContext.config, + auth_servers: [ + { id: "stage", url: "https://stage.example" }, + { id: "secondary", url: "https://secondary.example" }, + ], + }; + const client = toPublicConfig(config, "https://showcase.example").clients[0]; + expect(client.redirect_uri).toBe( + "https://showcase.example/auth/callback/login_ip/stage", + ); + expect(client).toHaveProperty("redirect_uris", { + stage: "https://showcase.example/auth/callback/login_ip/stage", + secondary: "https://showcase.example/auth/callback/login_ip/secondary", + }); + expect(JSON.stringify(client)).not.toContain("secret"); +}); diff --git a/tests/unit/token-response.test.ts b/tests/unit/token-response.test.ts new file mode 100644 index 0000000..dddf620 --- /dev/null +++ b/tests/unit/token-response.test.ts @@ -0,0 +1,54 @@ +import { test, expect } from "vitest"; +import { startProvider } from "../helpers/provider"; +import { exchangeCodeAndGetUserInfo } from "@/server/shared/http-client"; +import { verifySmsCode } from "@/server/ciba/provider"; +import { describeAuthFailure } from "@/server/auth/failure"; + +for (const malformed of [ + null, + [], + {}, + { access_token: "" }, + { access_token: 42 }, +]) { + test(`malformed token ${JSON.stringify(malformed)} fails at token without requesting userinfo`, async () => { + const provider = await startProvider(); + provider.behavior.tokenResponse = malformed; + try { + for (const exchange of [ + () => + exchangeCodeAndGetUserInfo( + provider.url + "/token", + provider.url + "/userinfo", + { code: "fixture" }, + ), + () => + verifySmsCode({ + authServerUrl: provider.url, + realm: "demo", + clientId: "fixture", + clientSecret: "fixture", + authReqId: "fixture", + smsCode: "123456", + }), + ]) { + provider.calls.length = 0; + let failure: unknown; + try { + await exchange(); + } catch (error) { + failure = error; + } + expect(describeAuthFailure(failure)).toMatchObject({ + reason: "TOKEN_RESPONSE_INVALID", + stage: "token", + }); + expect( + provider.calls.some((call) => call.operation === "userinfo"), + ).toBe(false); + } + } finally { + await provider.stop(); + } + }); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..8dff17a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,44 @@ +{ + "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", + ".next/dev/dev/types/**/*.ts" + ], + "exclude": [ + "node_modules", + ".superpowers", + ".local-backups" + ] +} 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 diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..d082bfa --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,18 @@ +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, + }, +});