diff --git a/.env.example b/.env.example index f674054..e6ca2ba 100644 --- a/.env.example +++ b/.env.example @@ -1,16 +1,21 @@ # NODE NODE_ENV=development +# SERVER +HOST=0.0.0.0 +PORT=5312 + # APPLICATION -VERSION=1.0.0 APP_NAME=Seamless Auth Example APP_ID=local-dev -APP_ORIGIN=http://localhost:3000 + +# Who can call this server +APP_ORIGINS=http://localhost:3000 + ISSUER=http://localhost:5312 # "web" for website to auth server, "server" for api server to auth server auth AUTH_MODE=server -DEMO=true # Roles assigned to every new user DEFAULT_ROLES=user,betaUser @@ -18,6 +23,9 @@ DEFAULT_ROLES=user,betaUser AVAILABLE_ROLES=user,admin,betaUser,team # DATABASE +# Prefer DATABASE_URL in containers and hosted environments if you already have one. +# DATABASE_URL=postgres://myuser:mypassword@localhost:5432/seamless_auth +# Set to true to see SQL from both the running app and startup migrations. DB_LOGGING=false DB_HOST=localhost DB_PORT=5432 @@ -31,20 +39,36 @@ REFRESH_TOKEN_TTL=1h RATE_LIMIT=100 DELAY_AFTER=50 -# OPTIONAL SERVICE TOKENS -# Used only if enabling admin or m2m communications +# SERVICE TOKENS +# Required when AUTH_MODE=server. API_SERVICE_TOKEN=32-byte-hex-string - -# JWKS -JWKS_ACTIVE_KID=dev-main +# Optional dedicated secret for indexed refresh-token lookup fingerprints. +# If unset, the server falls back to API_SERVICE_TOKEN, and in development only +# it will use a derived local secret. +REFRESH_TOKEN_LOOKUP_SECRET= # WEBAUTHN RPID=localhost ORIGINS=http://localhost:5173,http://localhost:5174 # ADMIN BOOTSTRAP -# Enables bootstrap feature SEAMLESS_BOOTSTRAP_ENABLED=true +SEAMLESS_BOOTSTRAP_SECRET=dev-bootstrap-secret-123 + +# OPTIONAL DIRECT DELIVERY +# Needed only if this auth API sends OTPs or magic links itself. +# Not needed when a SeamlessAuth server adapter handles external delivery. +# Set to true to exercise direct email/SMS delivery even when NODE_ENV=development. +MESSAGING_ENABLE_IN_DEV=false +MESSAGING_AWS_REGION=us-east-1 +MESSAGING_EMAIL_FROM=noreply@example.com +MESSAGING_SMS_PROVIDER=aws +MESSAGING_SMS_FROM= +MESSAGING_TWILIO_ACCOUNT_SID= +MESSAGING_TWILIO_AUTH_TOKEN= -# Secret used to authorize bootstrap invite creation -SEAMLESS_BOOTSTRAP_SECRET=dev-bootstrap-secret-123 \ No newline at end of file +# PRODUCTION SIGNING AND JWKS SECRETS +# Required when NODE_ENV=production. +# SEAMLESS_JWKS_ACTIVE_KID=main-2026-04 +# SEAMLESS_JWKS_KEY_main-2026-04_PRIVATE="-----BEGIN PRIVATE KEY-----..." +# JWKS_PUBLIC_KEYS={"keys":[{"kid":"main-2026-04","pem":"-----BEGIN PUBLIC KEY-----...","createdAt":"2026-04-22T00:00:00.000Z"}]} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..6c48c64 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,91 @@ +## Seamless Auth API Agent Guide + +This repository is an Express + TypeScript authentication service for passwordless auth flows. It supports WebAuthn/passkeys, OTP, magic links, JWKS publication, session management, admin endpoints, and a small system configuration layer persisted in Postgres. + +Use this file as the fast path. For the deeper architecture walkthrough, see [docs/architecture.md](/Users/brandoncorbett/git/seamless-auth-api/docs/architecture.md). + +## Start Here + +- Install dependencies: `npm install` +- Run in development: `npm run dev` +- Type-check/build: `npm run build` +- Lint: `npm run lint` +- Run tests once: `npm run test:run` +- Coverage: `npm run coverage` + +The service starts from [src/server.ts](/Users/brandoncorbett/git/seamless-auth-api/src/server.ts), which initializes models, connects to the database, bootstraps `system_config`, builds the Express app, and begins listening. + +## Core Runtime Shape + +- [src/app.ts](/Users/brandoncorbett/git/seamless-auth-api/src/app.ts): global middleware, CORS, OpenAPI docs in dev, rate limiting, and top-level error handlers. +- [src/lib/loadRoutes.ts](/Users/brandoncorbett/git/seamless-auth-api/src/lib/loadRoutes.ts): dynamically imports every `*.routes.ts` file under `src/routes`. +- [src/lib/createRouter.ts](/Users/brandoncorbett/git/seamless-auth-api/src/lib/createRouter.ts): thin wrapper used by route modules. +- [src/lib/defineRoute.ts](/Users/brandoncorbett/git/seamless-auth-api/src/lib/defineRoute.ts): registers Express handlers and OpenAPI metadata, validates request payloads, and optionally validates JSON responses. + +When tracing behavior, start at the route file, then the controller, then the service/model/helper layers. + +## Important Folders + +- [src/routes](/Users/brandoncorbett/git/seamless-auth-api/src/routes): endpoint registration +- [src/controllers](/Users/brandoncorbett/git/seamless-auth-api/src/controllers): request handling +- [src/services](/Users/brandoncorbett/git/seamless-auth-api/src/services): session issuance, messaging, bootstrap promotion, auth event logging +- [src/models](/Users/brandoncorbett/git/seamless-auth-api/src/models): Sequelize models +- [src/config](/Users/brandoncorbett/git/seamless-auth-api/src/config): bootstrapped system config, required config metadata +- [src/lib](/Users/brandoncorbett/git/seamless-auth-api/src/lib): routing, cookies, tokens, OpenAPI helpers +- [src/middleware](/Users/brandoncorbett/git/seamless-auth-api/src/middleware): auth, rate limits, logging +- [tests](/Users/brandoncorbett/git/seamless-auth-api/tests): unit, integration, e2e, and shared factories/setup + +## Auth Model + +There are three token states worth keeping straight: + +- Ephemeral token: short-lived pre-auth token used to continue registration/login flows. +- Access token: signed JWT used for authenticated application access. +- Refresh token: opaque random token stored hashed in the `sessions` table. + +Auth behavior depends on `AUTH_MODE`: + +- `web`: access/refresh/ephemeral tokens are primarily stored in cookies. +- `server`: endpoints expect bearer tokens more often and return token payloads in JSON. + +Auth middleware is chosen centrally by [src/middleware/attachAuthMiddleware.ts](/Users/brandoncorbett/git/seamless-auth-api/src/middleware/attachAuthMiddleware.ts). + +## Config Model + +There are two config layers: + +- Environment variables: needed to boot the process and connect external systems. +- `system_config` table: runtime configuration such as token TTLs, origins, roles, and WebAuthn RP settings. + +[src/config/bootstrapSystemConfig.ts](/Users/brandoncorbett/git/seamless-auth-api/src/config/bootstrapSystemConfig.ts) seeds missing `system_config` rows from env vars at startup. [src/config/getSystemConfig.ts](/Users/brandoncorbett/git/seamless-auth-api/src/config/getSystemConfig.ts) caches reads in-process, so remember to invalidate the cache after writes. + +## Messaging And Delivery + +OTP and magic link flows can operate in two modes: + +- Direct delivery: the API sends email/SMS itself via [src/services/messagingService.ts](/Users/brandoncorbett/git/seamless-auth-api/src/services/messagingService.ts). +- External delivery: callers send header `x-seamless-auth-delivery-mode: external` and the API returns delivery payloads instead of sending them directly. + +Direct provider wiring currently lives in [src/config/directMessaging.ts](/Users/brandoncorbett/git/seamless-auth-api/src/config/directMessaging.ts). + +## Working Safely In This Repo + +- Prefer changing controllers/services over adding logic directly in routes. +- When adding or updating routes, use `schemas`, not `schema`, so request parsing and OpenAPI generation both work. +- If a route requires auth, prefer the `auth` option in `createRouter` definitions. That keeps middleware wiring and OpenAPI security metadata aligned. +- If a route also needs admin checks or rate limiting, combine `auth` with extra `middleware`. +- Be careful around cookie names and auth mode branching. Several flows have separate `web` and `server` response shapes. +- Preserve existing local worktree changes unless the user explicitly asks you to clean them up. + +## Before You Finish A Change + +- Run `npm run build` +- Run `npm run lint` +- Run targeted tests for the touched area when practical +- If you change request/response schemas or route auth, inspect the generated `/openapi.json` behavior in dev + +## Known Maintenance Traps + +- OpenAPI security metadata is only added when routes use the `auth` field in route definitions. Routes that manually call `attachAuthMiddleware(...)` can drift from the documented security model. +- Cookie names in runtime code should be treated as the source of truth when checking auth behavior. +- `system_config` values may come from the database even when `.env` changes, so config bugs are not always fixed by editing env vars alone. diff --git a/LICENSE.md b/LICENSE.md deleted file mode 100644 index 1d15044..0000000 --- a/LICENSE.md +++ /dev/null @@ -1,26 +0,0 @@ -# License - -Seamless Auth API ("seamless-auth-api") is licensed under the **GNU Affero General Public License v3.0 (AGPL-3.0-only)**. - -- SPDX: `AGPL-3.0-only` - -## What this means (high level) - -- You are free to **use**, **modify**, and **self-host** this software. -- If you **modify** this software and **run it as a network service** (for example, hosting it for others to use), you must **make the complete corresponding source code of your modified version available** to users of that service, under the AGPL. - -This summary is not legal advice and does not replace the license text. - -## Full license text - -The full license text is available here: - -- https://www.gnu.org/licenses/agpl-3.0.html - -You should include a copy of the AGPLv3 license in your distribution. If this repository does not contain the full license text yet, add it as `LICENSE` or `LICENSE.txt` (recommended), and keep this `LICENSE.md` as the human-friendly summary. - -## Commercial licensing - -If you would like to embed Seamless Auth API into a proprietary product, redistribute it under different terms, or offer it as a managed service without AGPL obligations, commercial licensing may be available. - -Contact: support@seamlessauth.com diff --git a/README.md b/README.md index 9be8d79..40e3600 100644 --- a/README.md +++ b/README.md @@ -219,6 +219,11 @@ For production deployments: See [CONTRIBUTING.md](./CONTRIBUTING.md). +## Maintainer Docs + +- [AGENTS.md](./AGENTS.md) for a fast codebase briefing aimed at coding agents and maintainers +- [docs/architecture.md](./docs/architecture.md) for a deeper walkthrough of runtime flow, auth modes, config, and testing + ## Security **Do not open public issues for security vulnerabilities.** diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..fe6866a --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,245 @@ +# Seamless Auth API Architecture + +This document is the deeper companion to [AGENTS.md](/Users/brandoncorbett/git/seamless-auth-api/AGENTS.md). It explains how the service boots, how authentication flows move through the codebase, and which pieces tend to matter during maintenance. + +## 1. Boot Sequence + +Request handling is assembled in this order: + +1. [src/server.ts](/Users/brandoncorbett/git/seamless-auth-api/src/server.ts) +2. [src/models/index.ts](/Users/brandoncorbett/git/seamless-auth-api/src/models/index.ts) +3. [src/db.ts](/Users/brandoncorbett/git/seamless-auth-api/src/db.ts) +4. [src/config/bootstrapSystemConfig.ts](/Users/brandoncorbett/git/seamless-auth-api/src/config/bootstrapSystemConfig.ts) +5. [src/app.ts](/Users/brandoncorbett/git/seamless-auth-api/src/app.ts) +6. [src/lib/loadRoutes.ts](/Users/brandoncorbett/git/seamless-auth-api/src/lib/loadRoutes.ts) + +Important implications: + +- Models must initialize before route handlers run. +- The process will fail fast on missing database or required system configuration. +- Route registration is file-system driven, so every `*.routes.ts` file in `src/routes` is mounted automatically. + +## 2. Request Pipeline + +Global behavior is configured in [src/app.ts](/Users/brandoncorbett/git/seamless-auth-api/src/app.ts): + +- `helmet` +- JSON body parsing +- CORS +- cookie parsing +- request logging +- rate limiting and slow-down outside test mode +- development-only OpenAPI and Swagger UI +- generic error and 404 handlers + +Route modules use [src/lib/createRouter.ts](/Users/brandoncorbett/git/seamless-auth-api/src/lib/createRouter.ts) and [src/lib/defineRoute.ts](/Users/brandoncorbett/git/seamless-auth-api/src/lib/defineRoute.ts). `defineRoute` is more than syntactic sugar: + +- parses params/query/body with Zod +- registers OpenAPI metadata +- can validate JSON responses against Zod schemas +- optionally attaches auth middleware through the `auth` property + +If request parsing or OpenAPI output looks wrong, inspect the route definition before the controller. + +## 3. Main Auth Flows + +### Registration + +Primary files: + +- [src/routes/registration.routes.ts](/Users/brandoncorbett/git/seamless-auth-api/src/routes/registration.routes.ts) +- [src/controllers/registration.ts](/Users/brandoncorbett/git/seamless-auth-api/src/controllers/registration.ts) + +Behavior: + +- validates email/phone +- finds or creates the user +- issues an ephemeral token +- optionally sends or returns phone OTP delivery info +- stores bootstrap token in a cookie when present + +Registration does not itself create the long-lived session. It prepares the user for OTP, magic link, or WebAuthn completion. + +### OTP + +Primary files: + +- [src/routes/otp.routes.ts](/Users/brandoncorbett/git/seamless-auth-api/src/routes/otp.routes.ts) +- [src/controllers/otp.ts](/Users/brandoncorbett/git/seamless-auth-api/src/controllers/otp.ts) +- [src/utils/otp.ts](/Users/brandoncorbett/git/seamless-auth-api/src/utils/otp.ts) + +Behavior: + +- requires ephemeral auth for generation and verification endpoints +- supports both registration verification and login verification +- can either send messages directly or return delivery payloads to an external caller + +### Magic Link + +Primary files: + +- [src/routes/magicLink.routes.ts](/Users/brandoncorbett/git/seamless-auth-api/src/routes/magicLink.routes.ts) +- [src/controllers/magicLinks.ts](/Users/brandoncorbett/git/seamless-auth-api/src/controllers/magicLinks.ts) + +Behavior: + +- requires ephemeral auth to request the link +- stores hashed token plus device fingerprint data +- verification endpoint marks the token used +- polling endpoint finalizes the login/session if the same device later confirms it + +### WebAuthn / Passkeys + +Primary files: + +- [src/routes/webauthn.routes.ts](/Users/brandoncorbett/git/seamless-auth-api/src/routes/webauthn.routes.ts) +- [src/controllers/webauthn.ts](/Users/brandoncorbett/git/seamless-auth-api/src/controllers/webauthn.ts) + +Behavior: + +- start endpoints generate registration/authentication challenges +- finish endpoints verify browser responses with `@simplewebauthn/server` +- successful completion issues a real session +- registration/login completion can also trigger bootstrap admin promotion + +### Session Management + +Primary files: + +- [src/services/sessionIssuance.ts](/Users/brandoncorbett/git/seamless-auth-api/src/services/sessionIssuance.ts) +- [src/services/sessionService.ts](/Users/brandoncorbett/git/seamless-auth-api/src/services/sessionService.ts) +- [src/controllers/authentication.ts](/Users/brandoncorbett/git/seamless-auth-api/src/controllers/authentication.ts) +- [src/controllers/sessions.ts](/Users/brandoncorbett/git/seamless-auth-api/src/controllers/sessions.ts) + +Key concepts: + +- refresh tokens are opaque random values stored only as bcrypt hashes +- access tokens are signed JWTs using the JWKS-managed signing key +- cookie auth can silently refresh sessions +- session reuse detection revokes the replacement chain + +When debugging auth bugs, inspect both the token code and the `sessions` table behavior. The middleware validates both JWT claims and backing session state. + +## 4. Auth Modes + +`AUTH_MODE` changes the public contract of several endpoints. + +### `web` + +- tokens are primarily communicated via cookies +- cookie middleware is the normal auth path +- session issuance writes `seamless_access`, `seamless_refresh`, and `seamless_ephemeral` + +### `server` + +- more endpoints return token material in JSON +- bearer validation is used more heavily +- refresh endpoints expect bearer credentials rather than browser cookies + +When modifying a controller that returns auth state, verify both branches. Many regressions in this codebase would only show up in one mode. + +## 5. Config And Secrets + +### Environment variables + +The process relies on `.env` or runtime env vars for: + +- database connection +- issuer/origin metadata +- bootstrap toggles +- service token secrets +- production signing/JWKS material +- optional direct messaging provider credentials + +Reference points: + +- [.env.example](/Users/brandoncorbett/git/seamless-auth-api/.env.example) +- [validateEnvs.sh](/Users/brandoncorbett/git/seamless-auth-api/validateEnvs.sh) + +### `system_config` + +Bootstrapped runtime values include: + +- `app_name` +- `default_roles` +- `available_roles` +- token TTLs +- rate limiting config +- WebAuthn RP ID +- allowed origins + +Reference points: + +- [src/config/systemConfig.envMap.ts](/Users/brandoncorbett/git/seamless-auth-api/src/config/systemConfig.envMap.ts) +- [src/schemas/systemConfig.schema.ts](/Users/brandoncorbett/git/seamless-auth-api/src/schemas/systemConfig.schema.ts) +- [src/controllers/systemConfig.ts](/Users/brandoncorbett/git/seamless-auth-api/src/controllers/systemConfig.ts) + +The cache in `getSystemConfig()` is process-local. Any write path should invalidate it. + +## 6. Messaging + +Direct delivery lives in: + +- [src/services/messagingService.ts](/Users/brandoncorbett/git/seamless-auth-api/src/services/messagingService.ts) +- [src/config/directMessaging.ts](/Users/brandoncorbett/git/seamless-auth-api/src/config/directMessaging.ts) + +Supported direct transports: + +- AWS email +- AWS SMS +- Twilio SMS + +Flows can opt out of direct sending and instead return delivery payloads by sending `x-seamless-auth-delivery-mode: external`. + +This split is important when writing tests or integrating with an upstream orchestration service. + +## 7. Data Model Highlights + +Useful tables/models to understand early: + +- `users` +- `credentials` +- `sessions` +- `auth_events` +- `magic_links` +- `system_config` +- `bootstrap_invites` + +Model definitions live in [src/models](/Users/brandoncorbett/git/seamless-auth-api/src/models). Migrations live in [src/migrations](/Users/brandoncorbett/git/seamless-auth-api/src/migrations). + +## 8. Testing Strategy + +The test suite uses Vitest with: + +- unit tests for utilities, middleware, services, config, and OpenAPI generation +- integration tests for route/controller behavior +- e2e and smoke tests for higher-level flow coverage + +Reference points: + +- [vitest.config.ts](/Users/brandoncorbett/git/seamless-auth-api/vitest.config.ts) +- [tests/setup/env.ts](/Users/brandoncorbett/git/seamless-auth-api/tests/setup/env.ts) +- [tests/setup/globalSetup.ts](/Users/brandoncorbett/git/seamless-auth-api/tests/setup/globalSetup.ts) + +Practical guidance: + +- use unit tests for small auth helper changes +- use integration tests when touching controllers or middleware +- update OpenAPI tests if you change route schemas or docs generation + +## 9. Maintenance Notes And Sharp Edges + +- Some routes declare auth through `middleware: [attachAuthMiddleware(...)]` instead of the `auth` field. That works at runtime, but OpenAPI security metadata is only added by the `auth` field today. +- `defineRoute` expects `schemas`, plural. Using `schema` silently skips request parsing and docs wiring. +- Cookie names in runtime code are `seamless_access`, `seamless_refresh`, and `seamless_ephemeral`. Confirm docs and tests against those exact names. +- `system_config` can mask env changes after first bootstrap because the DB value becomes authoritative. +- Silent refresh and refresh-token matching depend on scanning active sessions and comparing bcrypt hashes, so session-heavy scenarios are worth extra care. + +## 10. Suggested Workflow For Agents + +1. Read the relevant route file. +2. Read the controller. +3. Read the service/helper/model touched by the controller. +4. Check the corresponding test file before editing. +5. If changing auth or schema behavior, inspect OpenAPI impact too. +6. Run `npm run build`, `npm run lint`, and targeted tests before wrapping up. diff --git a/package-lock.json b/package-lock.json index e078e9f..6eb8d92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,9 @@ "license": "AGPL-3.0-only", "dependencies": { "@asteasolutions/zod-to-openapi": "^8.4.3", + "@seamless-auth/messaging": "^0.1.0", + "@seamless-auth/messaging-aws": "^0.1.0", + "@seamless-auth/messaging-twilio": "^0.1.0", "@seamless-auth/types": "^0.1.3", "@sequelize/postgres": "^7.0.0-alpha.46", "@simplewebauthn/server": "^13.1.1", @@ -78,1924 +81,3253 @@ "zod": "^4.0.0" } }, - "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", - "dev": true, - "license": "MIT", + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" } }, - "node_modules/@babel/code-frame/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/@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==", - "dev": true, - "license": "MIT", + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, "engines": { - "node": ">=6.9.0" + "node": ">=14.0.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==", - "dev": true, - "license": "MIT", + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=6.9.0" + "node": ">=14.0.0" } }, - "node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", - "dev": true, - "license": "MIT", + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", "dependencies": { - "@babel/types": "^7.29.0" - }, - "bin": { - "parser": "bin/babel-parser.js" + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.0.0" + "node": ">=14.0.0" } }, - "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "dev": true, - "license": "MIT", + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=16.0.0" } }, - "node_modules/@bcoe/v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", - "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" } }, - "node_modules/@colors/colors": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", - "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", - "license": "MIT", - "engines": { - "node": ">=0.1.90" + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" } }, - "node_modules/@commander-js/extra-typings": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/@commander-js/extra-typings/-/extra-typings-13.1.0.tgz", - "integrity": "sha512-q5P52BYb1hwVWE6dtID7VvuJWrlfbCv4klj7BjUUOqMz4jbSZD4C9fJ9lRjL2jnBGTg+gDDlaXN51rkWcLk4fg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "commander": "~13.1.0" + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" } }, - "node_modules/@commitlint/cli": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-20.5.0.tgz", - "integrity": "sha512-yNkyN/tuKTJS3wdVfsZ2tXDM4G4Gi7z+jW54Cki8N8tZqwKBltbIvUUrSbT4hz1bhW/h0CdR+5sCSpXD+wMKaQ==", - "dev": true, - "license": "MIT", + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", "dependencies": { - "@commitlint/format": "^20.5.0", - "@commitlint/lint": "^20.5.0", - "@commitlint/load": "^20.5.0", - "@commitlint/read": "^20.5.0", - "@commitlint/types": "^20.5.0", - "tinyexec": "^1.0.0", - "yargs": "^17.0.0" + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" }, - "bin": { - "commitlint": "cli.js" + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=v18" + "node": ">=14.0.0" } }, - "node_modules/@commitlint/config-conventional": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-20.5.0.tgz", - "integrity": "sha512-t3Ni88rFw1XMa4nZHgOKJ8fIAT9M2j5TnKyTqJzsxea7FUetlNdYFus9dz+MhIRZmc16P0PPyEfh6X2d/qw8SA==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/client-ses": { + "version": "3.1034.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.1034.0.tgz", + "integrity": "sha512-yOkp3yqiVUrQpKfR9dmiEvRzQYJkm+6zAQjvVj12zvXoZtPvavvOau591gCZjau2xVe6RTpdlMSUkdm3fnTNXg==", + "license": "Apache-2.0", "dependencies": { - "@commitlint/types": "^20.5.0", - "conventional-changelog-conventionalcommits": "^9.2.0" + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.3", + "@aws-sdk/credential-provider-node": "^3.972.34", + "@aws-sdk/middleware-host-header": "^3.972.10", + "@aws-sdk/middleware-logger": "^3.972.10", + "@aws-sdk/middleware-recursion-detection": "^3.972.11", + "@aws-sdk/middleware-user-agent": "^3.972.33", + "@aws-sdk/region-config-resolver": "^3.972.13", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.8", + "@aws-sdk/util-user-agent-browser": "^3.972.10", + "@aws-sdk/util-user-agent-node": "^3.973.19", + "@smithy/config-resolver": "^4.4.17", + "@smithy/core": "^3.23.16", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/hash-node": "^4.2.14", + "@smithy/invalid-dependency": "^4.2.14", + "@smithy/middleware-content-length": "^4.2.14", + "@smithy/middleware-endpoint": "^4.4.31", + "@smithy/middleware-retry": "^4.5.4", + "@smithy/middleware-serde": "^4.2.19", + "@smithy/middleware-stack": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/node-http-handler": "^4.6.0", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.12", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.48", + "@smithy/util-defaults-mode-node": "^4.2.53", + "@smithy/util-endpoints": "^3.4.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.3", + "@smithy/util-utf8": "^4.2.2", + "@smithy/util-waiter": "^4.2.16", + "tslib": "^2.6.2" }, "engines": { - "node": ">=v18" + "node": ">=20.0.0" } }, - "node_modules/@commitlint/config-validator": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-20.5.0.tgz", - "integrity": "sha512-T/Uh6iJUzyx7j35GmHWdIiGRQB+ouZDk0pwAaYq4SXgB54KZhFdJ0vYmxiW6AMYICTIWuyMxDBl1jK74oFp/Gw==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/client-sns": { + "version": "3.1034.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sns/-/client-sns-3.1034.0.tgz", + "integrity": "sha512-Lk/GtcmWn47jtt23mDIDVoBKKuaRhvp0t3dX5fmGwwFGXx4I0rvGfC0AncW1SZkkNOIyd4a0odWyqNqJJRPiuw==", + "license": "Apache-2.0", "dependencies": { - "@commitlint/types": "^20.5.0", - "ajv": "^8.11.0" + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.3", + "@aws-sdk/credential-provider-node": "^3.972.34", + "@aws-sdk/middleware-host-header": "^3.972.10", + "@aws-sdk/middleware-logger": "^3.972.10", + "@aws-sdk/middleware-recursion-detection": "^3.972.11", + "@aws-sdk/middleware-user-agent": "^3.972.33", + "@aws-sdk/region-config-resolver": "^3.972.13", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.8", + "@aws-sdk/util-user-agent-browser": "^3.972.10", + "@aws-sdk/util-user-agent-node": "^3.973.19", + "@smithy/config-resolver": "^4.4.17", + "@smithy/core": "^3.23.16", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/hash-node": "^4.2.14", + "@smithy/invalid-dependency": "^4.2.14", + "@smithy/middleware-content-length": "^4.2.14", + "@smithy/middleware-endpoint": "^4.4.31", + "@smithy/middleware-retry": "^4.5.4", + "@smithy/middleware-serde": "^4.2.19", + "@smithy/middleware-stack": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/node-http-handler": "^4.6.0", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.12", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.48", + "@smithy/util-defaults-mode-node": "^4.2.53", + "@smithy/util-endpoints": "^3.4.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.3", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" }, "engines": { - "node": ">=v18" + "node": ">=20.0.0" } }, - "node_modules/@commitlint/ensure": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-20.5.0.tgz", - "integrity": "sha512-IpHqAUesBeW1EDDdjzJeaOxU9tnogLAyXLRBn03SHlj1SGENn2JGZqSWGkFvBJkJzfXAuCNtsoYzax+ZPS+puw==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/core": { + "version": "3.974.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.3.tgz", + "integrity": "sha512-W3aJJm2clu8OmsrwMOMnfof13O6LGnbknnZIQeSRbxjqKah2nVvkjbUBBZVhWrt08KC69H7WsINTdrxC/2SXQw==", + "license": "Apache-2.0", "dependencies": { - "@commitlint/types": "^20.5.0", - "lodash.camelcase": "^4.3.0", - "lodash.kebabcase": "^4.1.1", - "lodash.snakecase": "^4.1.1", - "lodash.startcase": "^4.4.0", - "lodash.upperfirst": "^4.3.1" + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/xml-builder": "^3.972.18", + "@smithy/core": "^3.23.16", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/signature-v4": "^5.3.14", + "@smithy/smithy-client": "^4.12.12", + "@smithy/types": "^4.14.1", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.3", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" }, "engines": { - "node": ">=v18" + "node": ">=20.0.0" } }, - "node_modules/@commitlint/execute-rule": { - "version": "20.0.0", - "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-20.0.0.tgz", - "integrity": "sha512-xyCoOShoPuPL44gVa+5EdZsBVao/pNzpQhkzq3RdtlFdKZtjWcLlUFQHSWBuhk5utKYykeJPSz2i8ABHQA+ZZw==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.29", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.29.tgz", + "integrity": "sha512-rf+AlUxgTeSzQ/4zoS0D+Bt7XvgpY48PnWG8Yg/N9fdMgyK2Jaqa+6tLZp4MYMIMHkGrfAxnbSeb2YLMGFMg6g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.3", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=v18" + "node": ">=20.0.0" } }, - "node_modules/@commitlint/format": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-20.5.0.tgz", - "integrity": "sha512-TI9EwFU/qZWSK7a5qyXMpKPPv3qta7FO4tKW+Wt2al7sgMbLWTsAcDpX1cU8k16TRdsiiet9aOw0zpvRXNJu7Q==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.31", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.31.tgz", + "integrity": "sha512-TR2/lQ3qKFj2EOrsiASzemsNEz2uzZ/SUBf48+U4Cr9a/FZlHfH/hwAeBJNBp1gMyJNxROJZhT3dn1cO+jnYfQ==", + "license": "Apache-2.0", "dependencies": { - "@commitlint/types": "^20.5.0", - "picocolors": "^1.1.1" + "@aws-sdk/core": "^3.974.3", + "@aws-sdk/types": "^3.973.8", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/node-http-handler": "^4.6.0", + "@smithy/property-provider": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.12", + "@smithy/types": "^4.14.1", + "@smithy/util-stream": "^4.5.24", + "tslib": "^2.6.2" }, "engines": { - "node": ">=v18" + "node": ">=20.0.0" } }, - "node_modules/@commitlint/is-ignored": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-20.5.0.tgz", - "integrity": "sha512-JWLarAsurHJhPozbuAH6GbP4p/hdOCoqS9zJMfqwswne+/GPs5V0+rrsfOkP68Y8PSLphwtFXV0EzJ+GTXTTGg==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.33", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.33.tgz", + "integrity": "sha512-UwdbJbOrgnOxZbshaNZ4DzX35h5wQd33MNYTGzWhN3ORG9lG9KQbDX6l6tDJSAdaGTktJoZPSritmUoW1rYkRA==", + "license": "Apache-2.0", "dependencies": { - "@commitlint/types": "^20.5.0", - "semver": "^7.6.0" + "@aws-sdk/core": "^3.974.3", + "@aws-sdk/credential-provider-env": "^3.972.29", + "@aws-sdk/credential-provider-http": "^3.972.31", + "@aws-sdk/credential-provider-login": "^3.972.33", + "@aws-sdk/credential-provider-process": "^3.972.29", + "@aws-sdk/credential-provider-sso": "^3.972.33", + "@aws-sdk/credential-provider-web-identity": "^3.972.33", + "@aws-sdk/nested-clients": "^3.997.1", + "@aws-sdk/types": "^3.973.8", + "@smithy/credential-provider-imds": "^4.2.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" }, "engines": { - "node": ">=v18" + "node": ">=20.0.0" } }, - "node_modules/@commitlint/lint": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-20.5.0.tgz", - "integrity": "sha512-jiM3hNUdu04jFBf1VgPdjtIPvbuVfDTBAc6L98AWcoLjF5sYqkulBHBzlVWll4rMF1T5zeQFB6r//a+s+BBKlA==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.33", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.33.tgz", + "integrity": "sha512-WyZuPVoDM1HGNl41eVg8HSSXIB+FGkuuK63GhDbh4TMdfWU03AciWvF/QqOVWvJtWVYaLddANJ+aUklVr2ieuw==", + "license": "Apache-2.0", "dependencies": { - "@commitlint/is-ignored": "^20.5.0", - "@commitlint/parse": "^20.5.0", - "@commitlint/rules": "^20.5.0", - "@commitlint/types": "^20.5.0" + "@aws-sdk/core": "^3.974.3", + "@aws-sdk/nested-clients": "^3.997.1", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" }, "engines": { - "node": ">=v18" + "node": ">=20.0.0" } }, - "node_modules/@commitlint/load": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-20.5.0.tgz", - "integrity": "sha512-sLhhYTL/KxeOTZjjabKDhwidGZan84XKK1+XFkwDYL/4883kIajcz/dZFAhBJmZPtL8+nBx6bnkzA95YxPeDPw==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.34", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.34.tgz", + "integrity": "sha512-sPcisURibKU4x0PCWJkWF1KJYm49Cph9dCn/PAnG5FU0wq5Id3g2v7RuEWAtNlKv1Af4gUJYBVGOeNpSEEx41A==", + "license": "Apache-2.0", "dependencies": { - "@commitlint/config-validator": "^20.5.0", - "@commitlint/execute-rule": "^20.0.0", - "@commitlint/resolve-extends": "^20.5.0", - "@commitlint/types": "^20.5.0", - "cosmiconfig": "^9.0.1", - "cosmiconfig-typescript-loader": "^6.1.0", - "is-plain-obj": "^4.1.0", - "lodash.mergewith": "^4.6.2", - "picocolors": "^1.1.1" + "@aws-sdk/credential-provider-env": "^3.972.29", + "@aws-sdk/credential-provider-http": "^3.972.31", + "@aws-sdk/credential-provider-ini": "^3.972.33", + "@aws-sdk/credential-provider-process": "^3.972.29", + "@aws-sdk/credential-provider-sso": "^3.972.33", + "@aws-sdk/credential-provider-web-identity": "^3.972.33", + "@aws-sdk/types": "^3.973.8", + "@smithy/credential-provider-imds": "^4.2.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" }, "engines": { - "node": ">=v18" + "node": ">=20.0.0" } }, - "node_modules/@commitlint/message": { - "version": "20.4.3", - "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-20.4.3.tgz", - "integrity": "sha512-6akwCYrzcrFcTYz9GyUaWlhisY4lmQ3KvrnabmhoeAV8nRH4dXJAh4+EUQ3uArtxxKQkvxJS78hNX2EU3USgxQ==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.29", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.29.tgz", + "integrity": "sha512-DURisqWS3bUgiwMXTmzymVNGlcRW0FnbPZ3SZknhmxnCXm3n9idkTJ6T+Uir359KRKtJNFLRViskk8HsSVLi1w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.3", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=v18" + "node": ">=20.0.0" } }, - "node_modules/@commitlint/parse": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-20.5.0.tgz", - "integrity": "sha512-SeKWHBMk7YOTnnEWUhx+d1a9vHsjjuo6Uo1xRfPNfeY4bdYFasCH1dDpAv13Lyn+dDPOels+jP6D2GRZqzc5fA==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.33", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.33.tgz", + "integrity": "sha512-9y9obU4IQWru9f+NiiscUeyCe5ZmQav4FKEb1qfUNrik/C3BzBGUnHQWyPEyXjOX9cb+vx1TYx0qZBtinKdzTA==", + "license": "Apache-2.0", "dependencies": { - "@commitlint/types": "^20.5.0", - "conventional-changelog-angular": "^8.2.0", - "conventional-commits-parser": "^6.3.0" + "@aws-sdk/core": "^3.974.3", + "@aws-sdk/nested-clients": "^3.997.1", + "@aws-sdk/token-providers": "3.1034.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" }, "engines": { - "node": ">=v18" + "node": ">=20.0.0" } }, - "node_modules/@commitlint/read": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-20.5.0.tgz", - "integrity": "sha512-JDEIJ2+GnWpK8QqwfmW7O42h0aycJEWNqcdkJnyzLD11nf9dW2dWLTVEa8Wtlo4IZFGLPATjR5neA5QlOvIH1w==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.33", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.33.tgz", + "integrity": "sha512-RazhlN0YAkna2T2p2v4YuuRlVBVRNo8V0SL+9JePTWDndEUAeOBAjYeQfAMbtDyCh120+zA0Op6V0jS4dw2+iw==", + "license": "Apache-2.0", "dependencies": { - "@commitlint/top-level": "^20.4.3", - "@commitlint/types": "^20.5.0", - "git-raw-commits": "^5.0.0", - "minimist": "^1.2.8", - "tinyexec": "^1.0.0" + "@aws-sdk/core": "^3.974.3", + "@aws-sdk/nested-clients": "^3.997.1", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" }, "engines": { - "node": ">=v18" + "node": ">=20.0.0" } }, - "node_modules/@commitlint/resolve-extends": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-20.5.0.tgz", - "integrity": "sha512-3SHPWUW2v0tyspCTcfSsYml0gses92l6TlogwzvM2cbxDgmhSRc+fldDjvGkCXJrjSM87BBaWYTPWwwyASZRrg==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.10.tgz", + "integrity": "sha512-IJSsIMeVQ8MMCPbuh1AbltkFhLBLXn7aejzfX5YKT/VLDHn++Dcz8886tXckE+wQssyPUhaXrJhdakO2VilRhg==", + "license": "Apache-2.0", "dependencies": { - "@commitlint/config-validator": "^20.5.0", - "@commitlint/types": "^20.5.0", - "global-directory": "^4.0.1", - "import-meta-resolve": "^4.0.0", - "lodash.mergewith": "^4.6.2", - "resolve-from": "^5.0.0" + "@aws-sdk/types": "^3.973.8", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" }, "engines": { - "node": ">=v18" + "node": ">=20.0.0" } }, - "node_modules/@commitlint/rules": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-20.5.0.tgz", - "integrity": "sha512-5NdQXQEdnDPT5pK8O39ZA7HohzPRHEsDGU23cyVCNPQy4WegAbAwrQk3nIu7p2sl3dutPk8RZd91yKTrMTnRkQ==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.10.tgz", + "integrity": "sha512-OOuGvvz1Dm20SjZo5oEBePFqxt5nf8AwkNDSyUHvD9/bfNASmstcYxFAHUowy4n6Io7mWUZ04JURZwSBvyQanQ==", + "license": "Apache-2.0", "dependencies": { - "@commitlint/ensure": "^20.5.0", - "@commitlint/message": "^20.4.3", - "@commitlint/to-lines": "^20.0.0", - "@commitlint/types": "^20.5.0" + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" }, "engines": { - "node": ">=v18" + "node": ">=20.0.0" } }, - "node_modules/@commitlint/to-lines": { - "version": "20.0.0", - "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-20.0.0.tgz", - "integrity": "sha512-2l9gmwiCRqZNWgV+pX1X7z4yP0b3ex/86UmUFgoRt672Ez6cAM2lOQeHFRUTuE6sPpi8XBCGnd8Kh3bMoyHwJw==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.972.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.11.tgz", + "integrity": "sha512-+zz6f79Kj9V5qFK2P+D8Ehjnw4AhphAlCAsPjUqEcInA9umtSSKMrHbSagEeOIsDNuvVrH98bjRHcyQukTrhaQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=v18" + "node": ">=20.0.0" } }, - "node_modules/@commitlint/top-level": { - "version": "20.4.3", - "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-20.4.3.tgz", - "integrity": "sha512-qD9xfP6dFg5jQ3NMrOhG0/w5y3bBUsVGyJvXxdWEwBm8hyx4WOk3kKXw28T5czBYvyeCVJgJJ6aoJZUWDpaacQ==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.972.32", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.32.tgz", + "integrity": "sha512-dc2O2x0V5pGJhmdQYQveUIFtMZsur7GrGuSgoKM4oQJuEcfvwnJ3sj+ip6WnxR5l6TrX5zkl4KgcgswOy3wAzQ==", + "license": "Apache-2.0", "dependencies": { - "escalade": "^3.2.0" + "@aws-sdk/core": "^3.974.3", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-arn-parser": "^3.972.3", + "@smithy/core": "^3.23.16", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/signature-v4": "^5.3.14", + "@smithy/smithy-client": "^4.12.12", + "@smithy/types": "^4.14.1", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-stream": "^4.5.24", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" }, "engines": { - "node": ">=v18" + "node": ">=20.0.0" } }, - "node_modules/@commitlint/types": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-20.5.0.tgz", - "integrity": "sha512-ZJoS8oSq2CAZEpc/YI9SulLrdiIyXeHb/OGqGrkUP6Q7YV+0ouNAa7GjqRdXeQPncHQIDz/jbCTlHScvYvO/gA==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.972.33", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.33.tgz", + "integrity": "sha512-mqtT3Fo7xanWMk2SbAcKLGGI/q1GHWNrExBj7cnWP2W2mkTMheXB4ntJvwPZ1UxPrQobrsv2dWFXmaOJeSOiDg==", + "license": "Apache-2.0", "dependencies": { - "conventional-commits-parser": "^6.3.0", - "picocolors": "^1.1.1" + "@aws-sdk/core": "^3.974.3", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.8", + "@smithy/core": "^3.23.16", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-retry": "^4.3.3", + "tslib": "^2.6.2" }, "engines": { - "node": ">=v18" + "node": ">=20.0.0" } }, - "node_modules/@conventional-changelog/git-client": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@conventional-changelog/git-client/-/git-client-2.6.0.tgz", - "integrity": "sha512-T+uPDciKf0/ioNNDpMGc8FDsehJClZP0yR3Q5MN6wE/Y/1QZ7F+80OgznnTCOlMEG4AV0LvH2UJi3C/nBnaBUg==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/nested-clients": { + "version": "3.997.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.1.tgz", + "integrity": "sha512-Afc9hc2WZs3X4Jb8dnxyuYiZsLoWRO51roTCRf497gPnAKN2WRdXANu1vaVCTzwnDMOYFXb/cYv4ZSjxqAqcKA==", + "license": "Apache-2.0", "dependencies": { - "@simple-libs/child-process-utils": "^1.0.0", - "@simple-libs/stream-utils": "^1.2.0", - "semver": "^7.5.2" + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.3", + "@aws-sdk/middleware-host-header": "^3.972.10", + "@aws-sdk/middleware-logger": "^3.972.10", + "@aws-sdk/middleware-recursion-detection": "^3.972.11", + "@aws-sdk/middleware-user-agent": "^3.972.33", + "@aws-sdk/region-config-resolver": "^3.972.13", + "@aws-sdk/signature-v4-multi-region": "^3.996.20", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.8", + "@aws-sdk/util-user-agent-browser": "^3.972.10", + "@aws-sdk/util-user-agent-node": "^3.973.19", + "@smithy/config-resolver": "^4.4.17", + "@smithy/core": "^3.23.16", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/hash-node": "^4.2.14", + "@smithy/invalid-dependency": "^4.2.14", + "@smithy/middleware-content-length": "^4.2.14", + "@smithy/middleware-endpoint": "^4.4.31", + "@smithy/middleware-retry": "^4.5.4", + "@smithy/middleware-serde": "^4.2.19", + "@smithy/middleware-stack": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/node-http-handler": "^4.6.0", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.12", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.48", + "@smithy/util-defaults-mode-node": "^4.2.53", + "@smithy/util-endpoints": "^3.4.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.3", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" }, "engines": { - "node": ">=18" + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.972.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.13.tgz", + "integrity": "sha512-CvJ2ZIjK/jVD/lbOpowBVElJyC1YxLTIJ13yM0AEo0t2v7swOzGjSA6lJGH+DwZXQhcjUjoYwc8bVYCX5MDr1A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/config-resolver": "^4.4.17", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" }, - "peerDependencies": { - "conventional-commits-filter": "^5.0.0", - "conventional-commits-parser": "^6.3.0" + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.20.tgz", + "integrity": "sha512-MEj6DhEcaO8RgVtFCJ+xpCQnZC3Iesr09avdY75qkMQfckQULu447IegK7Rs1MCGerVBfKnJQ4q+pQq9hI5lng==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "^3.972.32", + "@aws-sdk/types": "^3.973.8", + "@smithy/protocol-http": "^5.3.14", + "@smithy/signature-v4": "^5.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" }, - "peerDependenciesMeta": { - "conventional-commits-filter": { - "optional": true - }, - "conventional-commits-parser": { - "optional": true - } + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/token-providers": { + "version": "3.1034.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1034.0.tgz", + "integrity": "sha512-8E+KGcD4ET0H9FXJ2/ZWbfFnQNYEkTZZYJxAs1lkdJlve1AYuqaydInIFfvNgoz5GbYtzbK8/ugsSMu5wPm6kA==", + "license": "Apache-2.0", "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" + "@aws-sdk/core": "^3.974.3", + "@aws-sdk/nested-clients": "^3.997.1", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" }, "engines": { - "node": ">=12" + "node": ">=20.0.0" } }, - "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/types": { + "version": "3.973.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.8.tgz", + "integrity": "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==", + "license": "Apache-2.0", "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@dabh/diagnostics": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", - "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", - "license": "MIT", + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.3.tgz", + "integrity": "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==", + "license": "Apache-2.0", "dependencies": { - "@so-ric/colorspace": "^1.1.6", - "enabled": "2.0.x", - "kuler": "^2.0.0" + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@emnapi/core": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", - "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.996.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.8.tgz", + "integrity": "sha512-oOZHcRDihk5iEe5V25NVWg45b3qEA8OpHWVdU/XQh8Zj4heVPAJqWvMphQnU7LkufmUo10EpvFPZuQMiFLJK3g==", + "license": "Apache-2.0", "dependencies": { - "@emnapi/wasi-threads": "1.2.0", - "tslib": "^2.4.0" + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-endpoints": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@emnapi/runtime": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", - "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", + "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", + "license": "Apache-2.0", "dependencies": { - "tslib": "^2.4.0" + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", - "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.10.tgz", + "integrity": "sha512-FAzqXvfEssGdSIz8ejatan0bOdx1qefBWKF/gWmVBXIP1HkS7v/wjjaqrAGGKvyihrXTXW00/2/1nTJtxpXz7g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.973.19", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.19.tgz", + "integrity": "sha512-ZAfHjpzdbrzkAftC139JoYGfXzDh5HY+AxRzw8pGJ8cULsf+l721sKAMK8mV5NvRETaW/BwghSwQhGgoNgrxMw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "^3.972.33", + "@aws-sdk/types": "^3.973.8", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-config-provider": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.18.tgz", + "integrity": "sha512-BMDNVG1ETXRhl1tnisQiYBef3RShJ1kfZA7x7afivTFMLirfHNTb6U71K569HNXhSXbQZsweHvSDZ6euBw8hPA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "fast-xml-parser": "5.5.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "dev": true, "license": "MIT", - "optional": true, - "peer": true, "dependencies": { - "tslib": "^2.4.0" + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", - "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", - "cpu": [ - "ppc64" - ], + "node_modules/@babel/code-frame/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/@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==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "aix" - ], "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", - "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", - "cpu": [ - "arm" - ], + "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==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", - "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, "engines": { - "node": ">=18" + "node": ">=6.0.0" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", - "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", - "cpu": [ - "x64" - ], + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", - "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", - "cpu": [ - "arm64" - ], + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", - "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": ">=18" + "node": ">=0.1.90" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", - "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", - "cpu": [ - "arm64" - ], + "node_modules/@commander-js/extra-typings": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/@commander-js/extra-typings/-/extra-typings-13.1.0.tgz", + "integrity": "sha512-q5P52BYb1hwVWE6dtID7VvuJWrlfbCv4klj7BjUUOqMz4jbSZD4C9fJ9lRjL2jnBGTg+gDDlaXN51rkWcLk4fg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" + "peerDependencies": { + "commander": "~13.1.0" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", - "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", - "cpu": [ - "x64" - ], + "node_modules/@commitlint/cli": { + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-20.5.0.tgz", + "integrity": "sha512-yNkyN/tuKTJS3wdVfsZ2tXDM4G4Gi7z+jW54Cki8N8tZqwKBltbIvUUrSbT4hz1bhW/h0CdR+5sCSpXD+wMKaQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "dependencies": { + "@commitlint/format": "^20.5.0", + "@commitlint/lint": "^20.5.0", + "@commitlint/load": "^20.5.0", + "@commitlint/read": "^20.5.0", + "@commitlint/types": "^20.5.0", + "tinyexec": "^1.0.0", + "yargs": "^17.0.0" + }, + "bin": { + "commitlint": "cli.js" + }, "engines": { - "node": ">=18" + "node": ">=v18" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", - "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", - "cpu": [ - "arm" - ], + "node_modules/@commitlint/config-conventional": { + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-20.5.0.tgz", + "integrity": "sha512-t3Ni88rFw1XMa4nZHgOKJ8fIAT9M2j5TnKyTqJzsxea7FUetlNdYFus9dz+MhIRZmc16P0PPyEfh6X2d/qw8SA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@commitlint/types": "^20.5.0", + "conventional-changelog-conventionalcommits": "^9.2.0" + }, "engines": { - "node": ">=18" + "node": ">=v18" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", - "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", - "cpu": [ - "arm64" - ], + "node_modules/@commitlint/config-validator": { + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-20.5.0.tgz", + "integrity": "sha512-T/Uh6iJUzyx7j35GmHWdIiGRQB+ouZDk0pwAaYq4SXgB54KZhFdJ0vYmxiW6AMYICTIWuyMxDBl1jK74oFp/Gw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@commitlint/types": "^20.5.0", + "ajv": "^8.11.0" + }, "engines": { - "node": ">=18" + "node": ">=v18" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", - "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", - "cpu": [ - "ia32" - ], + "node_modules/@commitlint/ensure": { + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-20.5.0.tgz", + "integrity": "sha512-IpHqAUesBeW1EDDdjzJeaOxU9tnogLAyXLRBn03SHlj1SGENn2JGZqSWGkFvBJkJzfXAuCNtsoYzax+ZPS+puw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@commitlint/types": "^20.5.0", + "lodash.camelcase": "^4.3.0", + "lodash.kebabcase": "^4.1.1", + "lodash.snakecase": "^4.1.1", + "lodash.startcase": "^4.4.0", + "lodash.upperfirst": "^4.3.1" + }, "engines": { - "node": ">=18" + "node": ">=v18" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", - "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", - "cpu": [ - "loong64" - ], + "node_modules/@commitlint/execute-rule": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-20.0.0.tgz", + "integrity": "sha512-xyCoOShoPuPL44gVa+5EdZsBVao/pNzpQhkzq3RdtlFdKZtjWcLlUFQHSWBuhk5utKYykeJPSz2i8ABHQA+ZZw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=18" + "node": ">=v18" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", - "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", - "cpu": [ - "mips64el" - ], + "node_modules/@commitlint/format": { + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-20.5.0.tgz", + "integrity": "sha512-TI9EwFU/qZWSK7a5qyXMpKPPv3qta7FO4tKW+Wt2al7sgMbLWTsAcDpX1cU8k16TRdsiiet9aOw0zpvRXNJu7Q==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@commitlint/types": "^20.5.0", + "picocolors": "^1.1.1" + }, "engines": { - "node": ">=18" + "node": ">=v18" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", - "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", - "cpu": [ - "ppc64" - ], + "node_modules/@commitlint/is-ignored": { + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-20.5.0.tgz", + "integrity": "sha512-JWLarAsurHJhPozbuAH6GbP4p/hdOCoqS9zJMfqwswne+/GPs5V0+rrsfOkP68Y8PSLphwtFXV0EzJ+GTXTTGg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@commitlint/types": "^20.5.0", + "semver": "^7.6.0" + }, "engines": { - "node": ">=18" + "node": ">=v18" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", - "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", - "cpu": [ - "riscv64" - ], + "node_modules/@commitlint/lint": { + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-20.5.0.tgz", + "integrity": "sha512-jiM3hNUdu04jFBf1VgPdjtIPvbuVfDTBAc6L98AWcoLjF5sYqkulBHBzlVWll4rMF1T5zeQFB6r//a+s+BBKlA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@commitlint/is-ignored": "^20.5.0", + "@commitlint/parse": "^20.5.0", + "@commitlint/rules": "^20.5.0", + "@commitlint/types": "^20.5.0" + }, "engines": { - "node": ">=18" + "node": ">=v18" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", - "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", - "cpu": [ - "s390x" - ], + "node_modules/@commitlint/load": { + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-20.5.0.tgz", + "integrity": "sha512-sLhhYTL/KxeOTZjjabKDhwidGZan84XKK1+XFkwDYL/4883kIajcz/dZFAhBJmZPtL8+nBx6bnkzA95YxPeDPw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@commitlint/config-validator": "^20.5.0", + "@commitlint/execute-rule": "^20.0.0", + "@commitlint/resolve-extends": "^20.5.0", + "@commitlint/types": "^20.5.0", + "cosmiconfig": "^9.0.1", + "cosmiconfig-typescript-loader": "^6.1.0", + "is-plain-obj": "^4.1.0", + "lodash.mergewith": "^4.6.2", + "picocolors": "^1.1.1" + }, "engines": { - "node": ">=18" + "node": ">=v18" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", - "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", - "cpu": [ - "x64" - ], + "node_modules/@commitlint/message": { + "version": "20.4.3", + "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-20.4.3.tgz", + "integrity": "sha512-6akwCYrzcrFcTYz9GyUaWlhisY4lmQ3KvrnabmhoeAV8nRH4dXJAh4+EUQ3uArtxxKQkvxJS78hNX2EU3USgxQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=18" + "node": ">=v18" } }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", - "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", - "cpu": [ - "arm64" - ], + "node_modules/@commitlint/parse": { + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-20.5.0.tgz", + "integrity": "sha512-SeKWHBMk7YOTnnEWUhx+d1a9vHsjjuo6Uo1xRfPNfeY4bdYFasCH1dDpAv13Lyn+dDPOels+jP6D2GRZqzc5fA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], + "dependencies": { + "@commitlint/types": "^20.5.0", + "conventional-changelog-angular": "^8.2.0", + "conventional-commits-parser": "^6.3.0" + }, "engines": { - "node": ">=18" + "node": ">=v18" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", - "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", - "cpu": [ - "x64" - ], + "node_modules/@commitlint/read": { + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-20.5.0.tgz", + "integrity": "sha512-JDEIJ2+GnWpK8QqwfmW7O42h0aycJEWNqcdkJnyzLD11nf9dW2dWLTVEa8Wtlo4IZFGLPATjR5neA5QlOvIH1w==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], + "dependencies": { + "@commitlint/top-level": "^20.4.3", + "@commitlint/types": "^20.5.0", + "git-raw-commits": "^5.0.0", + "minimist": "^1.2.8", + "tinyexec": "^1.0.0" + }, "engines": { - "node": ">=18" + "node": ">=v18" } }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", - "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", - "cpu": [ - "arm64" - ], + "node_modules/@commitlint/resolve-extends": { + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-20.5.0.tgz", + "integrity": "sha512-3SHPWUW2v0tyspCTcfSsYml0gses92l6TlogwzvM2cbxDgmhSRc+fldDjvGkCXJrjSM87BBaWYTPWwwyASZRrg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], + "dependencies": { + "@commitlint/config-validator": "^20.5.0", + "@commitlint/types": "^20.5.0", + "global-directory": "^4.0.1", + "import-meta-resolve": "^4.0.0", + "lodash.mergewith": "^4.6.2", + "resolve-from": "^5.0.0" + }, "engines": { - "node": ">=18" + "node": ">=v18" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", - "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", - "cpu": [ - "x64" - ], + "node_modules/@commitlint/rules": { + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-20.5.0.tgz", + "integrity": "sha512-5NdQXQEdnDPT5pK8O39ZA7HohzPRHEsDGU23cyVCNPQy4WegAbAwrQk3nIu7p2sl3dutPk8RZd91yKTrMTnRkQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], + "dependencies": { + "@commitlint/ensure": "^20.5.0", + "@commitlint/message": "^20.4.3", + "@commitlint/to-lines": "^20.0.0", + "@commitlint/types": "^20.5.0" + }, "engines": { - "node": ">=18" + "node": ">=v18" } }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", - "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", - "cpu": [ - "arm64" - ], + "node_modules/@commitlint/to-lines": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-20.0.0.tgz", + "integrity": "sha512-2l9gmwiCRqZNWgV+pX1X7z4yP0b3ex/86UmUFgoRt672Ez6cAM2lOQeHFRUTuE6sPpi8XBCGnd8Kh3bMoyHwJw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], "engines": { - "node": ">=18" + "node": ">=v18" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", - "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", - "cpu": [ - "x64" - ], + "node_modules/@commitlint/top-level": { + "version": "20.4.3", + "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-20.4.3.tgz", + "integrity": "sha512-qD9xfP6dFg5jQ3NMrOhG0/w5y3bBUsVGyJvXxdWEwBm8hyx4WOk3kKXw28T5czBYvyeCVJgJJ6aoJZUWDpaacQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], + "dependencies": { + "escalade": "^3.2.0" + }, "engines": { - "node": ">=18" + "node": ">=v18" } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", - "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", - "cpu": [ - "arm64" - ], + "node_modules/@commitlint/types": { + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-20.5.0.tgz", + "integrity": "sha512-ZJoS8oSq2CAZEpc/YI9SulLrdiIyXeHb/OGqGrkUP6Q7YV+0ouNAa7GjqRdXeQPncHQIDz/jbCTlHScvYvO/gA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "conventional-commits-parser": "^6.3.0", + "picocolors": "^1.1.1" + }, "engines": { - "node": ">=18" + "node": ">=v18" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", - "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", - "cpu": [ - "ia32" - ], + "node_modules/@conventional-changelog/git-client": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@conventional-changelog/git-client/-/git-client-2.6.0.tgz", + "integrity": "sha512-T+uPDciKf0/ioNNDpMGc8FDsehJClZP0yR3Q5MN6wE/Y/1QZ7F+80OgznnTCOlMEG4AV0LvH2UJi3C/nBnaBUg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "@simple-libs/child-process-utils": "^1.0.0", + "@simple-libs/stream-utils": "^1.2.0", + "semver": "^7.5.2" + }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "conventional-commits-filter": "^5.0.0", + "conventional-commits-parser": "^6.3.0" + }, + "peerDependenciesMeta": { + "conventional-commits-filter": { + "optional": true + }, + "conventional-commits-parser": { + "optional": true + } } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", - "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "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==", + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", "dev": true, "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.4.3" + "@jridgewell/trace-mapping": "0.3.9" }, "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": ">=12" } }, - "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==", + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", "dev": true, "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "node_modules/@eslint/config-array": { - "version": "0.23.3", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz", - "integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==", + "node_modules/@dabh/diagnostics": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", + "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", + "license": "MIT", + "dependencies": { + "@so-ric/colorspace": "^1.1.6", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", + "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "peer": true, "dependencies": { - "@eslint/object-schema": "^3.0.3", - "debug": "^4.3.1", - "minimatch": "^10.2.4" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" } }, - "node_modules/@eslint/config-helpers": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz", - "integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==", + "node_modules/@emnapi/runtime": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "peer": true, "dependencies": { - "@eslint/core": "^1.1.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "tslib": "^2.4.0" } }, - "node_modules/@eslint/core": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz", - "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==", + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "peer": true, "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "tslib": "^2.4.0" } }, - "node_modules/@eslint/js": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", - "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" + "node": ">=18" } }, - "node_modules/@eslint/object-schema": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz", - "integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==", + "node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": ">=18" } }, - "node_modules/@eslint/plugin-kit": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz", - "integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==", + "node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.1.1", - "levn": "^0.4.1" - }, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": ">=18" } }, - "node_modules/@gar/promise-retry": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.3.tgz", - "integrity": "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA==", + "node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": ">=18" } }, - "node_modules/@hexagon/base64": { - "version": "1.1.28", - "resolved": "https://registry.npmjs.org/@hexagon/base64/-/base64-1.1.28.tgz", - "integrity": "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==", - "license": "MIT" - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=18.18.0" + "node": ">=18" } }, - "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.4.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=18.18.0" + "node": ">=18" } }, - "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==", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": ">=18" } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "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", - "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" - }, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@isaacs/cliui/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/@isaacs/cliui/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/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "dev": true, "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=18" } }, - "node_modules/@isaacs/cliui/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/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">=18" } }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.0.0" + "node": ">=18" } }, - "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==", + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6.0.0" + "node": ">=18" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@levischuck/tiny-cbor": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@levischuck/tiny-cbor/-/tiny-cbor-0.2.11.tgz", - "integrity": "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==", - "license": "MIT" - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz", - "integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==", + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" + "node": ">=18" } }, - "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==", + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 8" + "node": ">=18" } }, - "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==", + "node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 8" + "node": ">=18" } }, - "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==", + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">= 8" + "node": ">=18" } }, - "node_modules/@npmcli/agent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.0.tgz", - "integrity": "sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA==", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "ISC", + "license": "MIT", "optional": true, - "dependencies": { - "agent-base": "^7.1.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.1", - "lru-cache": "^11.2.1", - "socks-proxy-agent": "^8.0.3" - }, + "os": [ + "netbsd" + ], "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": ">=18" } }, - "node_modules/@npmcli/agent/node_modules/lru-cache": { - "version": "11.2.7", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", - "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT", "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": "20 || >=22" + "node": ">=18" } }, - "node_modules/@npmcli/fs": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-5.0.0.tgz", - "integrity": "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "ISC", + "license": "MIT", "optional": true, - "dependencies": { - "semver": "^7.3.5" - }, + "os": [ + "openbsd" + ], "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": ">=18" } }, - "node_modules/@npmcli/redact": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-4.0.0.tgz", - "integrity": "sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q==", + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "ISC", + "license": "MIT", "optional": true, + "os": [ + "openharmony" + ], "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": ">=18" } }, - "node_modules/@one-ini/wasm": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", - "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", - "license": "MIT" - }, - "node_modules/@oxc-project/types": { - "version": "0.122.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", - "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@paralleldrive/cuid2": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", - "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@noble/hashes": "^1.1.5" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@peculiar/asn1-android": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-android/-/asn1-android-2.6.0.tgz", - "integrity": "sha512-cBRCKtYPF7vJGN76/yG8VbxRcHLPF3HnkoHhKOZeHpoVtbMYfY9ROKtH3DtYUY9m8uI1Mh47PRhHf2hSK3xcSQ==", + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@peculiar/asn1-cms": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.1.tgz", - "integrity": "sha512-vdG4fBF6Lkirkcl53q6eOdn3XYKt+kJTG59edgRZORlg/3atWWEReRCx5rYE1ZzTTX6vLK5zDMjHh7vbrcXGtw==", + "node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "@peculiar/asn1-x509-attr": "^2.6.1", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@peculiar/asn1-csr": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.6.1.tgz", - "integrity": "sha512-WRWnKfIocHyzFYQTka8O/tXCiBquAPSrRjXbOkHbO4qdmS6loffCEGs+rby6WxxGdJCuunnhS2duHURhjyio6w==", + "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": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" + "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/@peculiar/asn1-ecc": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.6.1.tgz", - "integrity": "sha512-+Vqw8WFxrtDIN5ehUdvlN2m73exS2JVG0UAyfVB31gIfor3zWEAQPD+K9ydCxaj3MLen9k0JhKpu9LqviuCE1g==", + "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", - "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@peculiar/asn1-pfx": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.6.1.tgz", - "integrity": "sha512-nB5jVQy3MAAWvq0KY0R2JUZG8bO/bTLpnwyOzXyEh/e54ynGTatAR+csOnXkkVD9AFZ2uL8Z7EV918+qB1qDvw==", - "license": "MIT", + "node_modules/@eslint/config-array": { + "version": "0.23.3", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz", + "integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@peculiar/asn1-cms": "^2.6.1", - "@peculiar/asn1-pkcs8": "^2.6.1", - "@peculiar/asn1-rsa": "^2.6.1", - "@peculiar/asn1-schema": "^2.6.0", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" + "@eslint/object-schema": "^3.0.3", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@peculiar/asn1-pkcs8": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.6.1.tgz", + "node_modules/@eslint/config-helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz", + "integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz", + "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz", + "integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz", + "integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@gar/promise-retry": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.3.tgz", + "integrity": "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@hexagon/base64": { + "version": "1.1.28", + "resolved": "https://registry.npmjs.org/@hexagon/base64/-/base64-1.1.28.tgz", + "integrity": "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==", + "license": "MIT" + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.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/@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", + "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" + } + }, + "node_modules/@isaacs/cliui/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/@isaacs/cliui/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==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/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==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "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.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@levischuck/tiny-cbor": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@levischuck/tiny-cbor/-/tiny-cbor-0.2.11.tgz", + "integrity": "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==", + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz", + "integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "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==", + "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==", + "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==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/agent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.0.tgz", + "integrity": "sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^11.2.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "optional": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@npmcli/fs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-5.0.0.tgz", + "integrity": "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/redact": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-4.0.0.tgz", + "integrity": "sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q==", + "dev": true, + "license": "ISC", + "optional": true, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "license": "MIT" + }, + "node_modules/@oxc-project/types": { + "version": "0.122.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", + "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@peculiar/asn1-android": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-android/-/asn1-android-2.6.0.tgz", + "integrity": "sha512-cBRCKtYPF7vJGN76/yG8VbxRcHLPF3HnkoHhKOZeHpoVtbMYfY9ROKtH3DtYUY9m8uI1Mh47PRhHf2hSK3xcSQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-cms": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.1.tgz", + "integrity": "sha512-vdG4fBF6Lkirkcl53q6eOdn3XYKt+kJTG59edgRZORlg/3atWWEReRCx5rYE1ZzTTX6vLK5zDMjHh7vbrcXGtw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "@peculiar/asn1-x509-attr": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-csr": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.6.1.tgz", + "integrity": "sha512-WRWnKfIocHyzFYQTka8O/tXCiBquAPSrRjXbOkHbO4qdmS6loffCEGs+rby6WxxGdJCuunnhS2duHURhjyio6w==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-ecc": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.6.1.tgz", + "integrity": "sha512-+Vqw8WFxrtDIN5ehUdvlN2m73exS2JVG0UAyfVB31gIfor3zWEAQPD+K9ydCxaj3MLen9k0JhKpu9LqviuCE1g==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pfx": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.6.1.tgz", + "integrity": "sha512-nB5jVQy3MAAWvq0KY0R2JUZG8bO/bTLpnwyOzXyEh/e54ynGTatAR+csOnXkkVD9AFZ2uL8Z7EV918+qB1qDvw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.1", + "@peculiar/asn1-pkcs8": "^2.6.1", + "@peculiar/asn1-rsa": "^2.6.1", + "@peculiar/asn1-schema": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs8": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.6.1.tgz", "integrity": "sha512-JB5iQ9Izn5yGMw3ZG4Nw3Xn/hb/G38GYF3lf7WmJb8JZUydhVGEjK/ZlFSWhnlB7K/4oqEs8HnfFIKklhR58Tw==", "license": "MIT", "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs9": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.6.1.tgz", + "integrity": "sha512-5EV8nZoMSxeWmcxWmmcolg22ojZRgJg+Y9MX2fnE2bGRo5KQLqV5IL9kdSQDZxlHz95tHvIq9F//bvL1OeNILw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.1", + "@peculiar/asn1-pfx": "^2.6.1", + "@peculiar/asn1-pkcs8": "^2.6.1", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "@peculiar/asn1-x509-attr": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-rsa": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.6.1.tgz", + "integrity": "sha512-1nVMEh46SElUt5CB3RUTV4EG/z7iYc7EoaDY5ECwganibQPkZ/Y2eMsTKB/LeyrUJ+W/tKoD9WUqIy8vB+CEdA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.6.0.tgz", + "integrity": "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==", + "license": "MIT", + "dependencies": { + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.6.1.tgz", + "integrity": "sha512-O9jT5F1A2+t3r7C4VT7LYGXqkGLK7Kj1xFpz7U0isPrubwU5PbDoyYtx6MiGst29yq7pXN5vZbQFKRCP+lLZlA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509-attr": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.6.1.tgz", + "integrity": "sha512-tlW6cxoHwgcQghnJwv3YS+9OO1737zgPogZ+CgWRUK4roEwIPzRH4JEiG770xe5HX2ATfCpmX60gurfWIF9dcQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/x509": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz", + "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-csr": "^2.6.0", + "@peculiar/asn1-ecc": "^2.6.0", + "@peculiar/asn1-pkcs9": "^2.6.0", + "@peculiar/asn1-rsa": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "pvtsutils": "^1.3.6", + "reflect-metadata": "^0.2.2", + "tslib": "^2.8.1", + "tsyringe": "^4.10.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "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==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.11.tgz", + "integrity": "sha512-SJ+/g+xNnOh6NqYxD0V3uVN4W3VfnrGsC9/hoglicgTNfABFG9JjISvkkU0dNY84MNHLWyOgxP9v9Y9pX4S7+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.11.tgz", + "integrity": "sha512-7WQgR8SfOPwmDZGFkThUvsmd/nwAWv91oCO4I5LS7RKrssPZmOt7jONN0cW17ydGC1n/+puol1IpoieKqQidmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.11.tgz", + "integrity": "sha512-39Ks6UvIHq4rEogIfQBoBRusj0Q0nPVWIvqmwBLaT6aqQGIakHdESBVOPRRLacy4WwUPIx4ZKzfZ9PMW+IeyUQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.11.tgz", + "integrity": "sha512-jfsm0ZHfhiqrvWjJAmzsqiIFPz5e7mAoCOPBNTcNgkiid/LaFKiq92+0ojH+nmJmKYkre4t71BWXUZDNp7vsag==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.11.tgz", + "integrity": "sha512-zjQaUtSyq1nVe3nxmlSCuR96T1LPlpvmJ0SZy0WJFEsV4kFbXcq2u68L4E6O0XeFj4aex9bEauqjW8UQBeAvfQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.11.tgz", + "integrity": "sha512-WMW1yE6IOnehTcFE9eipFkm3XN63zypWlrJQ2iF7NrQ9b2LDRjumFoOGJE8RJJTJCTBAdmLMnJ8uVitACUUo1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.11.tgz", + "integrity": "sha512-jfndI9tsfm4APzjNt6QdBkYwre5lRPUgHeDHoI7ydKUuJvz3lZeCfMsI56BZj+7BYqiKsJm7cfd/6KYV7ubrBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.11.tgz", + "integrity": "sha512-ZlFgw46NOAGMgcdvdYwAGu2Q+SLFA9LzbJLW+iyMOJyhj5wk6P3KEE9Gct4xWwSzFoPI7JCdYmYMzVtlgQ+zfw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.11.tgz", + "integrity": "sha512-hIOYmuT6ofM4K04XAZd3OzMySEO4K0/nc9+jmNcxNAxRi6c5UWpqfw3KMFV4MVFWL+jQsSh+bGw2VqmaPMTLyw==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.11.tgz", + "integrity": "sha512-qXBQQO9OvkjjQPLdUVr7Nr2t3QTZI7s4KZtfw7HzBgjbmAPSFwSv4rmET9lLSgq3rH/ndA3ngv3Qb8l2njoPNA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.11.tgz", + "integrity": "sha512-/tpFfoSTzUkH9LPY+cYbqZBDyyX62w5fICq9qzsHLL8uTI6BHip3Q9Uzft0wylk/i8OOwKik8OxW+QAhDmzwmg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.11.tgz", + "integrity": "sha512-mcp3Rio2w72IvdZG0oQ4bM2c2oumtwHfUfKncUM6zGgz0KgPz4YmDPQfnXEiY5t3+KD/i8HG2rOB/LxdmieK2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.11.tgz", + "integrity": "sha512-LXk5Hii1Ph9asuGRjBuz8TUxdc1lWzB7nyfdoRgI0WGPZKmCxvlKk8KfYysqtr4MfGElu/f/pEQRh8fcEgkrWw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.11.tgz", + "integrity": "sha512-dDwf5otnx0XgRY1yqxOC4ITizcdzS/8cQ3goOWv3jFAo4F+xQYni+hnMuO6+LssHHdJW7+OCVL3CoU4ycnh35Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.11.tgz", + "integrity": "sha512-LN4/skhSggybX71ews7dAj6r2geaMJfm3kMbK2KhFMg9B10AZXnKoLCVVgzhMHL0S+aKtr4p8QbAW8k+w95bAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.11.tgz", + "integrity": "sha512-xQO9vbwBecJRv9EUcQ/y0dzSTJgA7Q6UVN7xp6B81+tBGSLVAK03yJ9NkJaUA7JFD91kbjxRSC/mDnmvXzbHoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, + "node_modules/@seamless-auth/messaging": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@seamless-auth/messaging/-/messaging-0.1.0.tgz", + "integrity": "sha512-a0buyNrnhcs12+Xb1xo1VGXPJNBmoxl5PIr2BPv/RZtb6pafGvtKgv+6DBM6QXyhuV9O+POm2ZVp7jLIYAKSoA==", + "license": "AGPL-3.0-only", + "engines": { + "node": ">=18" + } + }, + "node_modules/@seamless-auth/messaging-aws": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@seamless-auth/messaging-aws/-/messaging-aws-0.1.0.tgz", + "integrity": "sha512-kPLa6znztIx2IsFC2Bxf6M3wtVyV3D2Zp4qoYZfEdiWowjdH6wWLS5KUEREL/kR+pzDgeaDPXPcxOieDPYqdOQ==", + "license": "AGPL-3.0-only", + "dependencies": { + "@aws-sdk/client-ses": "^3.917.0", + "@aws-sdk/client-sns": "^3.917.0", + "@seamless-auth/messaging": "^0.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@seamless-auth/messaging-twilio": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@seamless-auth/messaging-twilio/-/messaging-twilio-0.1.0.tgz", + "integrity": "sha512-Eh5c8L1Le5hGsavwNz99YPEq7LA9h8/elBqTyFftAf6IF9GdQlDQfpF7BIigO22vHSHaS8xcpwbNSCuapyeyaQ==", + "license": "AGPL-3.0-only", + "dependencies": { + "@seamless-auth/messaging": "^0.1.0", + "twilio": "^5.10.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@seamless-auth/types": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@seamless-auth/types/-/types-0.1.3.tgz", + "integrity": "sha512-9Y2dq3clnXcSfoJ+pypt6FbMhXN75XLRu4+KuIFITl4OCSjuE8qudrF2nw21TG2meeadN0GHhAAYYzrng5tRoQ==", + "license": "AGPL-3.0", + "dependencies": { + "zod": "^4.3.6" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@sequelize/core": { + "version": "7.0.0-alpha.48", + "resolved": "https://registry.npmjs.org/@sequelize/core/-/core-7.0.0-alpha.48.tgz", + "integrity": "sha512-jRqJlgPo/H6qKpQY8ws+ULebRVtcvo/DDVBgf49Cn1Yd41xk+yZx3izuI9gLlZQQ75B/PRIbcP9WrHWAD8tJkg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/sequelize" + } + ], + "license": "MIT", + "dependencies": { + "@sequelize/utils": "7.0.0-alpha.48", + "@types/debug": "^4.1.12", + "@types/validator": "^13.15.10", + "ansis": "^3.17.0", + "bnf-parser": "^3.1.6", + "dayjs": "^1.11.19", + "debug": "^4.4.3", + "dottie": "^2.0.6", + "fast-glob": "^3.3.3", + "inflection": "^3.0.2", + "lodash": "^4.17.23", + "retry-as-promised": "^7.1.1", + "semver": "^7.7.3", + "sequelize-pool": "^8.0.1", + "toposort-class": "^1.0.1", + "type-fest": "^4.41.0", + "uuid": "^11.1.0", + "validator": "^13.15.26" + }, + "engines": { + "node": ">=18.20.8" } }, - "node_modules/@peculiar/asn1-pkcs9": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.6.1.tgz", - "integrity": "sha512-5EV8nZoMSxeWmcxWmmcolg22ojZRgJg+Y9MX2fnE2bGRo5KQLqV5IL9kdSQDZxlHz95tHvIq9F//bvL1OeNILw==", + "node_modules/@sequelize/postgres": { + "version": "7.0.0-alpha.48", + "resolved": "https://registry.npmjs.org/@sequelize/postgres/-/postgres-7.0.0-alpha.48.tgz", + "integrity": "sha512-Di7XMGDfK4H7Ne2e9WbfjriCyaRVauOe2wLITEQPv4w0Tnq5gmU28i1wJKkE6qjegkivz9X8Djm1KbDRZrBiGw==", "license": "MIT", "dependencies": { - "@peculiar/asn1-cms": "^2.6.1", - "@peculiar/asn1-pfx": "^2.6.1", - "@peculiar/asn1-pkcs8": "^2.6.1", - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "@peculiar/asn1-x509-attr": "^2.6.1", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" + "@sequelize/core": "7.0.0-alpha.48", + "@sequelize/utils": "7.0.0-alpha.48", + "@types/pg": "^8.11.14", + "lodash": "^4.17.23", + "pg": "^8.15.6", + "pg-hstore": "^2.3.4", + "pg-types": "^4.1.0", + "postgres-array": "^3.0.4", + "semver": "^7.7.3", + "wkx": "^0.5.0" } }, - "node_modules/@peculiar/asn1-rsa": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.6.1.tgz", - "integrity": "sha512-1nVMEh46SElUt5CB3RUTV4EG/z7iYc7EoaDY5ECwganibQPkZ/Y2eMsTKB/LeyrUJ+W/tKoD9WUqIy8vB+CEdA==", + "node_modules/@sequelize/utils": { + "version": "7.0.0-alpha.48", + "resolved": "https://registry.npmjs.org/@sequelize/utils/-/utils-7.0.0-alpha.48.tgz", + "integrity": "sha512-6s7YIDIRULddXuvdm4h6hbZtS6hXkqRzRzeG3WlzICmpY50ju0EC/4Ho3Ek4pYy7gl0IAs4HSmuOrmoUciT6Ng==", "license": "MIT", "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" + "@types/lodash": "^4.17.23", + "lodash": "^4.17.23" + }, + "engines": { + "node": ">=18.20.8" } }, - "node_modules/@peculiar/asn1-schema": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.6.0.tgz", - "integrity": "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==", + "node_modules/@simple-libs/child-process-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@simple-libs/child-process-utils/-/child-process-utils-1.0.2.tgz", + "integrity": "sha512-/4R8QKnd/8agJynkNdJmNw2MBxuFTRcNFnE5Sg/G+jkSsV8/UBgULMzhizWWW42p8L5H7flImV2ATi79Ove2Tw==", + "dev": true, "license": "MIT", "dependencies": { - "asn1js": "^3.0.6", - "pvtsutils": "^1.3.6", - "tslib": "^2.8.1" + "@simple-libs/stream-utils": "^1.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://ko-fi.com/dangreen" } }, - "node_modules/@peculiar/asn1-x509": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.6.1.tgz", - "integrity": "sha512-O9jT5F1A2+t3r7C4VT7LYGXqkGLK7Kj1xFpz7U0isPrubwU5PbDoyYtx6MiGst29yq7pXN5vZbQFKRCP+lLZlA==", + "node_modules/@simple-libs/stream-utils": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@simple-libs/stream-utils/-/stream-utils-1.2.0.tgz", + "integrity": "sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==", + "dev": true, "license": "MIT", - "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "asn1js": "^3.0.6", - "pvtsutils": "^1.3.6", - "tslib": "^2.8.1" + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://ko-fi.com/dangreen" } }, - "node_modules/@peculiar/asn1-x509-attr": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.6.1.tgz", - "integrity": "sha512-tlW6cxoHwgcQghnJwv3YS+9OO1737zgPogZ+CgWRUK4roEwIPzRH4JEiG770xe5HX2ATfCpmX60gurfWIF9dcQ==", + "node_modules/@simplewebauthn/server": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-13.3.0.tgz", + "integrity": "sha512-MLHYFrYG8/wK2i+86XMhiecK72nMaHKKt4bo+7Q1TbuG9iGjlSdfkPWKO5ZFE/BX+ygCJ7pr8H/AJeyAj1EaTQ==", "license": "MIT", "dependencies": { + "@hexagon/base64": "^1.1.27", + "@levischuck/tiny-cbor": "^0.2.2", + "@peculiar/asn1-android": "^2.6.0", + "@peculiar/asn1-ecc": "^2.6.1", + "@peculiar/asn1-rsa": "^2.6.1", "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.1", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" + "@peculiar/x509": "^1.14.3" + }, + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@peculiar/x509": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz", - "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==", - "license": "MIT", + "node_modules/@smithy/config-resolver": { + "version": "4.4.17", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.17.tgz", + "integrity": "sha512-TzDZcAnhTyAHbXVxWZo7/tEcrIeFq20IBk8So3OLOetWpR8EwY/yEqBMBFaJMeyEiREDq4NfEl+qO3OAUD+vbQ==", + "license": "Apache-2.0", "dependencies": { - "@peculiar/asn1-cms": "^2.6.0", - "@peculiar/asn1-csr": "^2.6.0", - "@peculiar/asn1-ecc": "^2.6.0", - "@peculiar/asn1-pkcs9": "^2.6.0", - "@peculiar/asn1-rsa": "^2.6.0", - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.0", - "pvtsutils": "^1.3.6", - "reflect-metadata": "^0.2.2", - "tslib": "^2.8.1", - "tsyringe": "^4.10.0" + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-endpoints": "^3.4.2", + "@smithy/util-middleware": "^4.2.14", + "tslib": "^2.6.2" }, "engines": { - "node": ">=20.0.0" + "node": ">=18.0.0" } }, - "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==", - "license": "MIT", - "optional": true, + "node_modules/@smithy/core": { + "version": "3.23.16", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.16.tgz", + "integrity": "sha512-JStomOrINQA1VqNEopLsgcdgwd42au7mykKqVr30XFw89wLt9sDxJDi4djVPRwQmmzyTGy/uOvTc2ultMpFi1w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-stream": "^4.5.24", + "@smithy/util-utf8": "^4.2.2", + "@smithy/uuid": "^1.1.2", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=14" + "node": ">=18.0.0" } }, - "node_modules/@pkgr/core": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", - "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", - "dev": true, - "license": "MIT", + "node_modules/@smithy/credential-provider-imds": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.14.tgz", + "integrity": "sha512-Au28zBN48ZAoXdooGUHemuVBrkE+Ie6RPmGNIAJsFqj33Vhb6xAgRifUydZ2aY+M+KaMAETAlKk5NC5h1G7wpg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "tslib": "^2.6.2" + }, "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.3.17", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.17.tgz", + "integrity": "sha512-bXOvQzaSm6MnmLaWA1elgfQcAtN4UP3vXqV97bHuoOrHQOJiLT3ds6o9eo5bqd0TJfRFpzdGnDQdW3FACiAVdw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.14", + "@smithy/querystring-builder": "^4.2.14", + "@smithy/types": "^4.14.1", + "@smithy/util-base64": "^4.3.2", + "tslib": "^2.6.2" }, - "funding": { - "url": "https://opencollective.com/pkgr" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.11.tgz", - "integrity": "sha512-SJ+/g+xNnOh6NqYxD0V3uVN4W3VfnrGsC9/hoglicgTNfABFG9JjISvkkU0dNY84MNHLWyOgxP9v9Y9pX4S7+A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "node_modules/@smithy/hash-node": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.14.tgz", + "integrity": "sha512-8ZBDY2DD4wr+GGjTpPtiglEsqr0lUP+KHqgZcWczFf6qeZ/YRjMIOoQWVQlmwu7EtxKTd8YXD8lblmYcpBIA1g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18.0.0" } }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.11.tgz", - "integrity": "sha512-7WQgR8SfOPwmDZGFkThUvsmd/nwAWv91oCO4I5LS7RKrssPZmOt7jONN0cW17ydGC1n/+puol1IpoieKqQidmg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "node_modules/@smithy/invalid-dependency": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.14.tgz", + "integrity": "sha512-c21qJiTSb25xvvOp+H2TNZzPCngrvl5vIPqPB8zQ/DmJF4QWXO19x1dWfMJZ6wZuuWUPPm0gV8C0cU3+ifcWuw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", + "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.14.tgz", + "integrity": "sha512-xhHq7fX4/3lv5NHxLUk3OeEvl0xZ+Ek3qIbWaCL4f9JwgDZEclPBElljaZCAItdGPQl/kSM4LPMOpy1MYgprpw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.4.31", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.31.tgz", + "integrity": "sha512-KJPdCIN2kOE2aGmqZd7eUTr4WQwOGgtLWgUkswGJggs7rBcQYQjcZMEDa3C0DwbOiXS9L8/wDoQHkfxBYLfiLw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.16", + "@smithy/middleware-serde": "^4.2.19", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-middleware": "^4.2.14", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.5.4.tgz", + "integrity": "sha512-/z7nIFK+ZRW3Ie/l3NEVGdy34LvmEOzBrtBAvgWZ/4PrKX0xP3kWm8pkfcwUk523SqxZhdbQP9JSXgjF77Uhpw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.16", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/service-error-classification": "^4.3.0", + "@smithy/smithy-client": "^4.12.12", + "@smithy/types": "^4.14.1", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.3", + "@smithy/uuid": "^1.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.2.19", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.19.tgz", + "integrity": "sha512-Q6y+W9h3iYVMCKWDoVge+OC1LKFqbEKaq8SIWG2X2bWJRpd/6dDLyICcNLT6PbjH3Rr6bmg/SeDB25XFOFfeEw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.16", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.14.tgz", + "integrity": "sha512-2dvkUKLuFdKsCRmOE4Mn63co0Djtsm+JMh0bYZQupN1pJwMeE8FmQmRLLzzEMN0dnNi7CDCYYH8F0EVwWiPBeA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.3.14", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.14.tgz", + "integrity": "sha512-S+gFjyo/weSVL0P1b9Ts8C/CwIfNCgUPikk3sl6QVsfE/uUuO+QsF+NsE/JkpvWqqyz1wg7HFdiaZuj5CoBMRg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.6.0.tgz", + "integrity": "sha512-P734cAoTFtuGfWa/R3jgBnGlURt2w9bYEBwQNMKf58sRM9RShirB2mKwLsVP+jlG/wxpCu8abv8NxdUts8tdLA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.14", + "@smithy/querystring-builder": "^4.2.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.14.tgz", + "integrity": "sha512-WuM31CgfsnQ/10i7NYr0PyxqknD72Y5uMfUMVSniPjbEPceiTErb4eIqJQ+pdxNEAUEWrewrGjIRjVbVHsxZiQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18.0.0" } }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.11.tgz", - "integrity": "sha512-39Ks6UvIHq4rEogIfQBoBRusj0Q0nPVWIvqmwBLaT6aqQGIakHdESBVOPRRLacy4WwUPIx4ZKzfZ9PMW+IeyUQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "node_modules/@smithy/protocol-http": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.14.tgz", + "integrity": "sha512-dN5F8kHx8RNU0r+pCwNmFZyz6ChjMkzShy/zup6MtkRmmix4vZzJdW+di7x//b1LiynIev88FM18ie+wwPcQtQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18.0.0" } }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.11.tgz", - "integrity": "sha512-jfsm0ZHfhiqrvWjJAmzsqiIFPz5e7mAoCOPBNTcNgkiid/LaFKiq92+0ojH+nmJmKYkre4t71BWXUZDNp7vsag==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "node_modules/@smithy/querystring-builder": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.14.tgz", + "integrity": "sha512-XYA5Z0IqTeF+5XDdh4BBmSA0HvbgVZIyv4cmOoUheDNR57K1HgBp9ukUMx3Cr3XpDHHpLBnexPE3LAtDsZkj2A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "@smithy/util-uri-escape": "^4.2.2", + "tslib": "^2.6.2" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18.0.0" } }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.11.tgz", - "integrity": "sha512-zjQaUtSyq1nVe3nxmlSCuR96T1LPlpvmJ0SZy0WJFEsV4kFbXcq2u68L4E6O0XeFj4aex9bEauqjW8UQBeAvfQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@smithy/querystring-parser": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.14.tgz", + "integrity": "sha512-hr+YyqBD23GVvRxGGrcc/oOeNlK3PzT5Fu4dzrDXxzS1LpFiuL2PQQqKPs87M79aW7ziMs+nvB3qdw77SqE7Lw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18.0.0" } }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.11.tgz", - "integrity": "sha512-WMW1yE6IOnehTcFE9eipFkm3XN63zypWlrJQ2iF7NrQ9b2LDRjumFoOGJE8RJJTJCTBAdmLMnJ8uVitACUUo1Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@smithy/service-error-classification": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.3.0.tgz", + "integrity": "sha512-9jKsBYQRPR0xBLgc2415RsA5PIcP2sis4oBdN9s0D13cg1B1284mNTjx9Yc+BEERXzuPm5ObktI96OxsKh8E9A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18.0.0" } }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.11.tgz", - "integrity": "sha512-jfndI9tsfm4APzjNt6QdBkYwre5lRPUgHeDHoI7ydKUuJvz3lZeCfMsI56BZj+7BYqiKsJm7cfd/6KYV7ubrBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.9.tgz", + "integrity": "sha512-495/V2I15SHgedSJoDPD23JuSfKAp726ZI1V0wtjB07Wh7q/0tri/0e0DLefZCHgxZonrGKt/OCTpAtP1wE1kQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18.0.0" } }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.11.tgz", - "integrity": "sha512-ZlFgw46NOAGMgcdvdYwAGu2Q+SLFA9LzbJLW+iyMOJyhj5wk6P3KEE9Gct4xWwSzFoPI7JCdYmYMzVtlgQ+zfw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@smithy/signature-v4": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.14.tgz", + "integrity": "sha512-1D9Y/nmlVjCeSivCbhZ7hgEpmHyY1h0GvpSZt3l0xcD9JjmjVC1CHOozS6+Gh+/ldMH8JuJ6cujObQqfayAVFA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.2", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-uri-escape": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18.0.0" } }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.11.tgz", - "integrity": "sha512-hIOYmuT6ofM4K04XAZd3OzMySEO4K0/nc9+jmNcxNAxRi6c5UWpqfw3KMFV4MVFWL+jQsSh+bGw2VqmaPMTLyw==", - "cpu": [ - "s390x" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@smithy/smithy-client": { + "version": "4.12.12", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.12.tgz", + "integrity": "sha512-daO7SJn4eM6ArbmrEs+/BTbH7af8AEbSL3OMQdcRvvn8tuUcR5rU2n6DgxIV53aXMS42uwK8NgKKCh5XgqYOPQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.16", + "@smithy/middleware-endpoint": "^4.4.31", + "@smithy/middleware-stack": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-stream": "^4.5.24", + "tslib": "^2.6.2" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18.0.0" } }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.11.tgz", - "integrity": "sha512-qXBQQO9OvkjjQPLdUVr7Nr2t3QTZI7s4KZtfw7HzBgjbmAPSFwSv4rmET9lLSgq3rH/ndA3ngv3Qb8l2njoPNA==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@smithy/types": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.1.tgz", + "integrity": "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18.0.0" } }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.11.tgz", - "integrity": "sha512-/tpFfoSTzUkH9LPY+cYbqZBDyyX62w5fICq9qzsHLL8uTI6BHip3Q9Uzft0wylk/i8OOwKik8OxW+QAhDmzwmg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@smithy/url-parser": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.14.tgz", + "integrity": "sha512-p06BiBigJ8bTA3MgnOfCtDUWnAMY0YfedO/GRpmc7p+wg3KW8vbXy1xwSu5ASy0wV7rRYtlfZOIKH4XqfhjSQQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.2.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18.0.0" } }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.11.tgz", - "integrity": "sha512-mcp3Rio2w72IvdZG0oQ4bM2c2oumtwHfUfKncUM6zGgz0KgPz4YmDPQfnXEiY5t3+KD/i8HG2rOB/LxdmieK2g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], + "node_modules/@smithy/util-base64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.2.tgz", + "integrity": "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18.0.0" } }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.11.tgz", - "integrity": "sha512-LXk5Hii1Ph9asuGRjBuz8TUxdc1lWzB7nyfdoRgI0WGPZKmCxvlKk8KfYysqtr4MfGElu/f/pEQRh8fcEgkrWw==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.2.tgz", + "integrity": "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==", + "license": "Apache-2.0", "dependencies": { - "@napi-rs/wasm-runtime": "^1.1.1" + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.11.tgz", - "integrity": "sha512-dDwf5otnx0XgRY1yqxOC4ITizcdzS/8cQ3goOWv3jFAo4F+xQYni+hnMuO6+LssHHdJW7+OCVL3CoU4ycnh35Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "node_modules/@smithy/util-body-length-node": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.3.tgz", + "integrity": "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18.0.0" } }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.11.tgz", - "integrity": "sha512-LN4/skhSggybX71ews7dAj6r2geaMJfm3kMbK2KhFMg9B10AZXnKoLCVVgzhMHL0S+aKtr4p8QbAW8k+w95bAA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "node_modules/@smithy/util-buffer-from": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", + "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.2.tgz", + "integrity": "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18.0.0" } }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.11", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.11.tgz", - "integrity": "sha512-xQO9vbwBecJRv9EUcQ/y0dzSTJgA7Q6UVN7xp6B81+tBGSLVAK03yJ9NkJaUA7JFD91kbjxRSC/mDnmvXzbHoQ==", - "dev": true, - "license": "MIT" + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.3.48", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.48.tgz", + "integrity": "sha512-hxVRVPYaRDWa6YQdse1aWX1qrksmLsvNyGBKdc32q4jFzSjxYVNWfstknAfR228TnzS4tzgswXRuYIbhXBuXFQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.14", + "@smithy/smithy-client": "^4.12.12", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } }, - "node_modules/@scarf/scarf": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", - "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", - "hasInstallScript": true, - "license": "Apache-2.0" + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.2.53", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.53.tgz", + "integrity": "sha512-ybgCk+9JdBq8pYC8Y6U5fjyS8e4sboyAShetxPNL0rRBtaVl56GSFAxsolVBIea1tXR4LPIzL8i6xqmcf0+DCQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.4.17", + "@smithy/credential-provider-imds": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/smithy-client": "^4.12.12", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } }, - "node_modules/@seamless-auth/types": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@seamless-auth/types/-/types-0.1.3.tgz", - "integrity": "sha512-9Y2dq3clnXcSfoJ+pypt6FbMhXN75XLRu4+KuIFITl4OCSjuE8qudrF2nw21TG2meeadN0GHhAAYYzrng5tRoQ==", - "license": "AGPL-3.0", + "node_modules/@smithy/util-endpoints": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.4.2.tgz", + "integrity": "sha512-a55Tr+3OKld4TTtnT+RhKOQHyPxm3j/xL4OR83WBUhLJaKDS9dnJ7arRMOp3t31dcLhApwG9bgvrRXBHlLdIkg==", + "license": "Apache-2.0", "dependencies": { - "zod": "^4.3.6" + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" }, "engines": { - "node": ">=20" + "node": ">=18.0.0" } }, - "node_modules/@sequelize/core": { - "version": "7.0.0-alpha.48", - "resolved": "https://registry.npmjs.org/@sequelize/core/-/core-7.0.0-alpha.48.tgz", - "integrity": "sha512-jRqJlgPo/H6qKpQY8ws+ULebRVtcvo/DDVBgf49Cn1Yd41xk+yZx3izuI9gLlZQQ75B/PRIbcP9WrHWAD8tJkg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/sequelize" - } - ], - "license": "MIT", + "node_modules/@smithy/util-hex-encoding": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.2.tgz", + "integrity": "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==", + "license": "Apache-2.0", "dependencies": { - "@sequelize/utils": "7.0.0-alpha.48", - "@types/debug": "^4.1.12", - "@types/validator": "^13.15.10", - "ansis": "^3.17.0", - "bnf-parser": "^3.1.6", - "dayjs": "^1.11.19", - "debug": "^4.4.3", - "dottie": "^2.0.6", - "fast-glob": "^3.3.3", - "inflection": "^3.0.2", - "lodash": "^4.17.23", - "retry-as-promised": "^7.1.1", - "semver": "^7.7.3", - "sequelize-pool": "^8.0.1", - "toposort-class": "^1.0.1", - "type-fest": "^4.41.0", - "uuid": "^11.1.0", - "validator": "^13.15.26" + "tslib": "^2.6.2" }, "engines": { - "node": ">=18.20.8" + "node": ">=18.0.0" } }, - "node_modules/@sequelize/postgres": { - "version": "7.0.0-alpha.48", - "resolved": "https://registry.npmjs.org/@sequelize/postgres/-/postgres-7.0.0-alpha.48.tgz", - "integrity": "sha512-Di7XMGDfK4H7Ne2e9WbfjriCyaRVauOe2wLITEQPv4w0Tnq5gmU28i1wJKkE6qjegkivz9X8Djm1KbDRZrBiGw==", - "license": "MIT", + "node_modules/@smithy/util-middleware": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.14.tgz", + "integrity": "sha512-1Su2vj9RYNDEv/V+2E+jXkkwGsgR7dc4sfHn9Z7ruzQHJIEni9zzw5CauvRXlFJfmgcqYP8fWa0dkh2Q2YaQyw==", + "license": "Apache-2.0", "dependencies": { - "@sequelize/core": "7.0.0-alpha.48", - "@sequelize/utils": "7.0.0-alpha.48", - "@types/pg": "^8.11.14", - "lodash": "^4.17.23", - "pg": "^8.15.6", - "pg-hstore": "^2.3.4", - "pg-types": "^4.1.0", - "postgres-array": "^3.0.4", - "semver": "^7.7.3", - "wkx": "^0.5.0" + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@sequelize/utils": { - "version": "7.0.0-alpha.48", - "resolved": "https://registry.npmjs.org/@sequelize/utils/-/utils-7.0.0-alpha.48.tgz", - "integrity": "sha512-6s7YIDIRULddXuvdm4h6hbZtS6hXkqRzRzeG3WlzICmpY50ju0EC/4Ho3Ek4pYy7gl0IAs4HSmuOrmoUciT6Ng==", - "license": "MIT", + "node_modules/@smithy/util-retry": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.3.3.tgz", + "integrity": "sha512-idjUvd4M9Jj6rXkhqw4H4reHoweuK4ZxYWyOrEp4N2rOF5VtaOlQGLDQJva/8WanNXk9ScQtsAb7o5UHGvFm4A==", + "license": "Apache-2.0", "dependencies": { - "@types/lodash": "^4.17.23", - "lodash": "^4.17.23" + "@smithy/service-error-classification": "^4.3.0", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" }, "engines": { - "node": ">=18.20.8" + "node": ">=18.0.0" } }, - "node_modules/@simple-libs/child-process-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@simple-libs/child-process-utils/-/child-process-utils-1.0.2.tgz", - "integrity": "sha512-/4R8QKnd/8agJynkNdJmNw2MBxuFTRcNFnE5Sg/G+jkSsV8/UBgULMzhizWWW42p8L5H7flImV2ATi79Ove2Tw==", - "dev": true, - "license": "MIT", + "node_modules/@smithy/util-stream": { + "version": "4.5.24", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.24.tgz", + "integrity": "sha512-na5vv2mBSDzXewLEEoWGI7LQQkfpmFEomBsmOpzLFjqGctm0iMwXY5lAwesY9pIaErkccW0qzEOUcYP+WKneXg==", + "license": "Apache-2.0", "dependencies": { - "@simple-libs/stream-utils": "^1.2.0" + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/node-http-handler": "^4.6.0", + "@smithy/types": "^4.14.1", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" }, "engines": { - "node": ">=18" + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz", + "integrity": "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" }, - "funding": { - "url": "https://ko-fi.com/dangreen" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@simple-libs/stream-utils": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@simple-libs/stream-utils/-/stream-utils-1.2.0.tgz", - "integrity": "sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==", - "dev": true, - "license": "MIT", + "node_modules/@smithy/util-utf8": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", + "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.2", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "4.2.16", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.16.tgz", + "integrity": "sha512-GtclrKoZ3Lt7jPQ7aTIYKfjY92OgceScftVnkTsG8e1KV8rkvZgN+ny6YSRhd9hxB8rZtwVbmln7NTvE5O3GmQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" }, - "funding": { - "url": "https://ko-fi.com/dangreen" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@simplewebauthn/server": { - "version": "13.3.0", - "resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-13.3.0.tgz", - "integrity": "sha512-MLHYFrYG8/wK2i+86XMhiecK72nMaHKKt4bo+7Q1TbuG9iGjlSdfkPWKO5ZFE/BX+ygCJ7pr8H/AJeyAj1EaTQ==", - "license": "MIT", + "node_modules/@smithy/uuid": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.2.tgz", + "integrity": "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==", + "license": "Apache-2.0", "dependencies": { - "@hexagon/base64": "^1.1.27", - "@levischuck/tiny-cbor": "^0.2.2", - "@peculiar/asn1-android": "^2.6.0", - "@peculiar/asn1-ecc": "^2.6.1", - "@peculiar/asn1-rsa": "^2.6.1", - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "@peculiar/x509": "^1.14.3" + "tslib": "^2.6.2" }, "engines": { - "node": ">=20.0.0" + "node": ">=18.0.0" } }, "node_modules/@so-ric/colorspace": { @@ -2990,7 +4322,6 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, "license": "MIT" }, "node_modules/at-least-node": { @@ -3002,6 +4333,17 @@ "node": ">= 4.0.0" } }, + "node_modules/axios": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz", + "integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -3124,6 +4466,12 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", @@ -3526,7 +4874,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -3831,7 +5178,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -4121,7 +5467,6 @@ "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==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -4710,6 +6055,41 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fast-xml-builder": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.5.tgz", + "integrity": "sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.1.3" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.5.8", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.8.tgz", + "integrity": "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "fast-xml-builder": "^1.1.4", + "path-expression-matcher": "^1.2.0", + "strnum": "^2.2.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -4852,6 +6232,26 @@ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", "license": "MIT" }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -4872,7 +6272,6 @@ "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -5201,7 +6600,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -7119,6 +8517,21 @@ "node": ">=8" } }, + "node_modules/path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -7520,6 +8933,15 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -7862,6 +9284,13 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/scmp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/scmp/-/scmp-2.1.0.tgz", + "integrity": "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==", + "deprecated": "Just use Node.js's crypto.timingSafeEqual()", + "license": "BSD-3-Clause" + }, "node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", @@ -8627,6 +10056,18 @@ "node": ">=0.10.0" } }, + "node_modules/strnum": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz", + "integrity": "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/superagent": { "version": "10.3.0", "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz", @@ -9005,6 +10446,49 @@ "node": "*" } }, + "node_modules/twilio": { + "version": "5.13.1", + "resolved": "https://registry.npmjs.org/twilio/-/twilio-5.13.1.tgz", + "integrity": "sha512-sT+PkhptF4Mf7t8eXFFvPQx4w5VHnBIPXbltGPMFRe+R2GxfRdMuFbuNA/cEm0aQR6LFQOn33+fhClg+TjRVqQ==", + "license": "MIT", + "dependencies": { + "axios": "^1.13.5", + "dayjs": "^1.11.9", + "https-proxy-agent": "^5.0.0", + "jsonwebtoken": "^9.0.3", + "qs": "^6.14.1", + "scmp": "^2.1.0", + "xmlbuilder": "^13.0.2" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/twilio/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/twilio/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -9578,6 +11062,15 @@ "dev": true, "license": "ISC" }, + "node_modules/xmlbuilder": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-13.0.2.tgz", + "integrity": "sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==", + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 21c730f..08132f8 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,9 @@ "homepage": "https://github.com/fells-code/seamless-auth-api#readme", "dependencies": { "@asteasolutions/zod-to-openapi": "^8.4.3", + "@seamless-auth/messaging": "^0.1.0", + "@seamless-auth/messaging-aws": "^0.1.0", + "@seamless-auth/messaging-twilio": "^0.1.0", "@seamless-auth/types": "^0.1.3", "@sequelize/postgres": "^7.0.0-alpha.46", "@simplewebauthn/server": "^13.1.1", diff --git a/src/app.ts b/src/app.ts index a56d0a7..352b95a 100644 --- a/src/app.ts +++ b/src/app.ts @@ -95,7 +95,7 @@ export async function createApp() { user_agent: req.headers['user-agent'], metadata: { reason: 'Request from an unexpected origin' }, }); - res.setHeader('Access-Control-Allow-Origin', process.env.APP_ORIGIN!); + res.setHeader('Access-Control-Allow-Origin', rawOrigin[0]); return res.status(403).json({ message: 'CORS policy does not allow this origin.' }); } return next(); @@ -115,13 +115,17 @@ export async function createApp() { app.use((req: Request, res: Response) => { logger.warn( - `[${req.ip}] didn't make it anywhere. Path: ${req.path}. Tracking of suspicous behavior`, + `[${req.ip}] ${req.method} ${req.originalUrl} did not match any route. Tracking suspicious behavior`, ); AuthEvent.create({ type: 'request_suspicous', ip_address: req.ip, user_agent: req.headers['user-agent'], - metadata: { reason: 'Request to an unknown route.' }, + metadata: { + reason: 'Request to an unknown route.', + method: req.method, + path: req.originalUrl, + }, }); return res.status(404).json({ error: 'Not Found' }); }); diff --git a/src/config/config.cjs b/src/config/config.cjs index 9bf47e5..7641164 100644 --- a/src/config/config.cjs +++ b/src/config/config.cjs @@ -4,6 +4,14 @@ * See LICENSE file in the project root for full license information */ +function dbLoggingOption() { + if (process.env.DB_LOGGING !== 'true') { + return false; + } + + return (sql) => console.debug(sql); +} + module.exports = { development: { username: process.env.DB_USER, @@ -12,7 +20,7 @@ module.exports = { host: process.env.DB_HOST, port: process.env.DB_PORT, dialect: 'postgres', - logging: 'false', + logging: dbLoggingOption(), }, test: { username: process.env.DB_USER, @@ -29,6 +37,6 @@ module.exports = { host: process.env.DB_HOST, port: process.env.DB_PORT, dialect: 'postgres', - logging: 'false', + logging: dbLoggingOption(), }, }; diff --git a/src/config/directMessaging.ts b/src/config/directMessaging.ts new file mode 100644 index 0000000..007a78c --- /dev/null +++ b/src/config/directMessaging.ts @@ -0,0 +1,116 @@ +/* + * Copyright © 2026 Fells Code, LLC + * Licensed under the GNU Affero General Public License v3.0 + * See LICENSE file in the project root for full license information + */ + +import { + type AuthMessagingService, + createAuthMessagingService, + MessagingConfigurationError, +} from '@seamless-auth/messaging'; +import { createAwsEmailTransport, createAwsSmsTransport } from '@seamless-auth/messaging-aws'; +import { createTwilioSmsTransport } from '@seamless-auth/messaging-twilio'; + +export type DirectSmsProvider = 'aws' | 'twilio'; + +function getAwsRegion(): string | undefined { + return process.env.MESSAGING_AWS_REGION ?? process.env.AWS_REGION ?? process.env.REGION; +} + +function getEmailFrom(): string | undefined { + return process.env.MESSAGING_EMAIL_FROM ?? process.env.SES_EMAIL; +} + +function getSmsProvider(): DirectSmsProvider { + const rawProvider = ( + process.env.MESSAGING_SMS_PROVIDER ?? + process.env.SMS_PROVIDER ?? + 'aws' + ).toLowerCase(); + + if (rawProvider !== 'aws' && rawProvider !== 'twilio') { + throw new MessagingConfigurationError( + `Unsupported MESSAGING_SMS_PROVIDER "${rawProvider}". Expected "aws" or "twilio".`, + ); + } + + return rawProvider; +} + +function getTwilioAccountSid(): string | undefined { + return process.env.MESSAGING_TWILIO_ACCOUNT_SID ?? process.env.TWILIO_ACCOUNT_SID; +} + +function getTwilioAuthToken(): string | undefined { + return process.env.MESSAGING_TWILIO_AUTH_TOKEN ?? process.env.TWILIO_AUTH_TOKEN; +} + +function getSmsFrom(): string | undefined { + return process.env.MESSAGING_SMS_FROM ?? process.env.TWILIO_PHONE_NUMBER; +} + +function buildEmailTransport() { + const region = getAwsRegion(); + const fromEmail = getEmailFrom(); + + if (!region) { + throw new MessagingConfigurationError( + 'MESSAGING_AWS_REGION or AWS_REGION is required for direct email delivery.', + ); + } + + if (!fromEmail) { + throw new MessagingConfigurationError( + 'MESSAGING_EMAIL_FROM is required for direct email delivery.', + ); + } + + return createAwsEmailTransport({ + region, + fromEmail, + }); +} + +function buildSmsTransport() { + const provider = getSmsProvider(); + + if (provider === 'aws') { + const region = getAwsRegion(); + + if (!region) { + throw new MessagingConfigurationError( + 'MESSAGING_AWS_REGION or AWS_REGION is required when MESSAGING_SMS_PROVIDER=aws.', + ); + } + + return createAwsSmsTransport({ + region, + senderId: getSmsFrom(), + }); + } + + const accountSid = getTwilioAccountSid(); + const authToken = getTwilioAuthToken(); + const fromNumber = getSmsFrom(); + + if (!accountSid || !authToken || !fromNumber) { + throw new MessagingConfigurationError( + 'MESSAGING_TWILIO_ACCOUNT_SID, MESSAGING_TWILIO_AUTH_TOKEN, and MESSAGING_SMS_FROM are required when MESSAGING_SMS_PROVIDER=twilio.', + ); + } + + return createTwilioSmsTransport({ + accountSid, + authToken, + fromNumber, + }); +} + +export function createDirectAuthMessagingService(appName: string): AuthMessagingService { + return createAuthMessagingService({ + appName, + email: buildEmailTransport(), + sms: buildSmsTransport(), + }); +} diff --git a/src/controllers/authentication.ts b/src/controllers/authentication.ts index 0e8fbe5..6ed8c99 100644 --- a/src/controllers/authentication.ts +++ b/src/controllers/authentication.ts @@ -4,14 +4,12 @@ * See LICENSE file in the project root for full license information */ -import { compareSync } from 'bcrypt-ts'; import { Request, Response } from 'express'; -import jwt from 'jsonwebtoken'; -import { Op } from 'sequelize'; import { getSystemConfig } from '../config/getSystemConfig.js'; import { clearAuthCookies, setAuthCookies } from '../lib/cookie.js'; import { + createRefreshTokenLookup, generateRefreshToken, hashRefreshToken, signAccessToken, @@ -22,14 +20,18 @@ import { Credential } from '../models/credentials.js'; import { Session } from '../models/sessions.js'; import { User } from '../models/users.js'; import { AuthEventService } from '../services/authEventService.js'; -import { hardRevokeSession, revokeSessionChain } from '../services/sessionService.js'; +import { + findRefreshSessionByToken, + hardRevokeSession, + revokeSessionChain, +} from '../services/sessionService.js'; import { AuthenticatedRequest } from '../types/types.js'; import getLogger from '../utils/logger.js'; -import { getSecret } from '../utils/secretsStore.js'; import { computeSessionTimes, isValidEmail, isValidPhoneNumber, + normalizePhoneNumber, parseDurationToSeconds, } from '../utils/utils.js'; @@ -40,6 +42,10 @@ export const login = async (req: Request, res: Response) => { // For the initial login step, user either passes in an email or a phone number const { identifier, passkeyAvailable } = req.body; let user, identifierType; + const normalizedIdentifier = + typeof identifier === 'string' && isValidPhoneNumber(identifier) + ? normalizePhoneNumber(identifier) + : null; if (!identifier) { logger.warn('No pre authenticated identifier found'); @@ -73,10 +79,10 @@ export const login = async (req: Request, res: Response) => { }); return res.status(401).json({ message: 'Not allowed' }); } - } else if (isValidPhoneNumber(identifier)) { + } else if (isValidPhoneNumber(identifier) && normalizedIdentifier) { try { user = await User.findOne({ - where: { phone: identifier }, + where: { phone: normalizedIdentifier }, }); identifierType = 'phone'; } catch { @@ -225,64 +231,64 @@ export const logout = async (req: Request, res: Response) => { }; export const refreshSession = async (req: Request, res: Response) => { - const authReq = req as AuthenticatedRequest; - const authUser = authReq.user; logger.info(`Refreshing user token`); let refreshToken: string | null = null; - if (req.headers.authorization?.startsWith('Bearer ')) { + if (AUTH_MODE === 'server' && req.headers.authorization?.startsWith('Bearer ')) { refreshToken = req.headers.authorization.slice('Bearer '.length); } + if (!refreshToken && AUTH_MODE === 'web') { + refreshToken = req.cookies?.seamless_refresh ?? null; + } + if (!refreshToken) { logger.error('Refresh token provided is not of expected type for auth server configurations'); await AuthEventService.log({ - userId: authUser.id, - type: 'bearer_token_suspicious', + userId: null, + type: 'refresh_token_failed', req, - metadata: { reason: 'Missing all required headers and tokens needed to perform a refresh' }, + metadata: { reason: 'Missing refresh token' }, }); res.status(401).json({ error: 'Not allowed' }); return; } - const serviceSecret = await getSecret('API_SERVICE_TOKEN'); - - const payload = jwt.verify(refreshToken, serviceSecret, { - issuer: process.env.APP_ORIGIN, - audience: process.env.ISSUER, - }) as jwt.JwtPayload; - const now = new Date(); + const { session, legacyFallbackCandidates, usedLegacyFallback } = await findRefreshSessionByToken( + refreshToken, + now, + ); - // Find session that is not revoked, not replaced, and not expired - const candidateSessions = await Session.findAll({ - where: { - revokedAt: null, - expiresAt: { [Op.gt]: now }, - idleExpiresAt: { [Op.gt]: now }, - }, - }); + if (!session) { + const looksLikeJwt = refreshToken.split('.').length === 3; + + logger.warn( + `No refresh session found for refresh token. legacyFallbackCandidates=${legacyFallbackCandidates} tokenFormat=${looksLikeJwt ? 'jwt_like' : 'opaque'}`, + ); - let session: Session | null = null; - for (const s of candidateSessions) { - const match = await compareSync(payload.refreshToken, s.refreshTokenHash); - if (match) { - session = s; - break; + if (looksLikeJwt) { + logger.warn( + 'Refresh endpoint received a JWT-shaped bearer token. Server-mode /refresh expects the raw opaque refresh token, not the access token.', + ); } - } - if (!session) { - logger.warn('No refresh session found for refresh token'); await AuthEventService.serviceTokenInvalid(req); return res.status(401).json({ error: 'invalid_refresh_token' }); } + if (usedLegacyFallback) { + logger.info( + `Refresh token matched a legacy session without refreshTokenLookup. sessionId=${session.id} fallbackCandidates=${legacyFallbackCandidates}`, + ); + } + // Reuse detection: if this session was already rotated, it means we’ve seen this token before if (session.replacedBySessionId || session.revokedAt) { - logger.warn('Token reuse detected'); + logger.warn( + `Token reuse detected for session ${session.id}. replacedBySessionId=${session.replacedBySessionId ?? 'none'} revokedAt=${session.revokedAt ? session.revokedAt.toISOString() : 'null'}. This usually means an already-rotated refresh token was sent again.`, + ); // Reuse -> revoke session chain await revokeSessionChain(session); // Log security event @@ -302,12 +308,14 @@ export const refreshSession = async (req: Request, res: Response) => { const { expiresAt, idleExpiresAt } = computeSessionTimes(now); const newRefreshToken = generateRefreshToken(); const newRefreshTokenHash = await hashRefreshToken(newRefreshToken); + const newRefreshTokenLookup = createRefreshTokenLookup(newRefreshToken); const newSession = await Session.create({ userId: user.id, infraId: session.infraId, mode: session.mode, refreshTokenHash: newRefreshTokenHash, + refreshTokenLookup: newRefreshTokenLookup, userAgent: session.userAgent, ipAddress: req.ip, expiresAt, @@ -317,9 +325,12 @@ export const refreshSession = async (req: Request, res: Response) => { session.replacedBySessionId = newSession.id; await session.save(); - const token = await signAccessToken(session.id, user.id, user.roles); + const token = await signAccessToken(newSession.id, user.id, user.roles); - if (token && newRefreshTokenHash) { + if (token && newRefreshToken) { + logger.info( + `Refresh token rotated for user ${user.id}. oldSessionId=${session.id} newSessionId=${newSession.id}`, + ); await AuthEventService.log({ userId: user.id, type: 'refresh_token_success', req }); if (AUTH_MODE === 'web') { @@ -332,10 +343,15 @@ export const refreshSession = async (req: Request, res: Response) => { return res.status(200).json({ message: 'Success', token, - refreshToken: newRefreshTokenHash, + refreshToken: newRefreshToken, sub: user.id, + roles: user.roles, + email: user.email, + phone: user.phone, ttl: parseDurationToSeconds(access_token_ttl || '15m'), refreshTtl: parseDurationToSeconds(refresh_token_ttl || '1h'), }); } + + return res.status(500).json({ error: 'Failed to refresh session' }); }; diff --git a/src/controllers/bootstrap.ts b/src/controllers/bootstrap.ts index 3cd34a0..031a09f 100644 --- a/src/controllers/bootstrap.ts +++ b/src/controllers/bootstrap.ts @@ -15,6 +15,7 @@ import { import getLogger from '../utils/logger.js'; const logger = getLogger('bootstrapAdminInvite'); +const EXTERNAL_DELIVERY_HEADER = 'x-seamless-auth-delivery-mode'; function getBearerToken(req: Request): string | undefined { const auth = req.header('authorization'); @@ -26,6 +27,10 @@ function getBearerToken(req: Request): string | undefined { return token; } +function wantsExternalDelivery(req: Request) { + return req.get(EXTERNAL_DELIVERY_HEADER)?.toLowerCase() === 'external'; +} + export async function createAdminBootstrapInviteHandler(req: Request, res: Response) { try { logger.info('Creating a bootstrap admin invitation'); @@ -36,11 +41,13 @@ export async function createAdminBootstrapInviteHandler(req: Request, res: Respo await assertBootstrapAllowed(); const { email } = req.body; + const useExternalDelivery = wantsExternalDelivery(req); const result = await createAdminBootstrapInvite({ email, createdIp: req.ip ?? null, createdUserAgent: req.get('user-agent') ?? null, + sendMessage: !useExternalDelivery, }); return res.status(201).json({ @@ -49,6 +56,16 @@ export async function createAdminBootstrapInviteHandler(req: Request, res: Respo url: result.registrationUrl, expiresAt: result.expiresAt.toISOString(), token: result.token, + ...(useExternalDelivery + ? { + delivery: { + kind: 'bootstrap_invite_email', + to: result.email, + inviteUrl: result.registrationUrl, + token: result.token, + }, + } + : {}), }, }); } catch (error) { diff --git a/src/controllers/magicLinks.ts b/src/controllers/magicLinks.ts index f125c78..f5e0d76 100644 --- a/src/controllers/magicLinks.ts +++ b/src/controllers/magicLinks.ts @@ -24,10 +24,16 @@ const logger = getLogger('magic-links'); const TTL_MINUTES = 15; const AUTH_MODE: 'web' | 'server' = process.env.AUTH_MODE! as 'web' | 'server'; +const EXTERNAL_DELIVERY_HEADER = 'x-seamless-auth-delivery-mode'; + +function wantsExternalDelivery(req: Request) { + return req.get(EXTERNAL_DELIVERY_HEADER)?.toLowerCase() === 'external'; +} export async function requestMagicLink(req: Request, res: Response) { const authReq = req as AuthenticatedRequest; const preAuthUser = authReq.user; + const useExternalDelivery = wantsExternalDelivery(req); const user = await User.findOne({ where: { email: preAuthUser.email } }); @@ -68,7 +74,9 @@ export async function requestMagicLink(req: Request, res: Response) { expires_at: new Date(Date.now() + TTL_MINUTES * 60 * 1000), }); - await sendMagicLinkEmail(user.email, rawToken, redirect_url); + if (!useExternalDelivery) { + await sendMagicLinkEmail(user.email, rawToken, redirect_url); + } await AuthEventService.log({ userId: user.id, @@ -78,6 +86,16 @@ export async function requestMagicLink(req: Request, res: Response) { return res.json({ message: 'If an account exists, a login link has been sent.', + ...(useExternalDelivery + ? { + delivery: { + kind: 'magic_link_email', + to: user.email, + token: rawToken, + magicLinkUrl: redirect_url, + }, + } + : {}), }); } diff --git a/src/controllers/otp.ts b/src/controllers/otp.ts index 2e4727f..c5410a3 100644 --- a/src/controllers/otp.ts +++ b/src/controllers/otp.ts @@ -18,15 +18,22 @@ import { verifyEmailOTP, verifyPhoneOTP, } from '../utils/otp.js'; -import { isValidEmail, isValidPhoneNumber } from '../utils/utils.js'; +import { isValidEmail, isValidPhoneNumber, normalizePhoneNumber } from '../utils/utils.js'; const logger = getLogger('otp'); const AUTH_MODE: 'web' | 'server' = process.env.AUTH_MODE! as 'web' | 'server'; +const EXTERNAL_DELIVERY_HEADER = 'x-seamless-auth-delivery-mode'; + +function wantsExternalDelivery(req: Request) { + return req.get(EXTERNAL_DELIVERY_HEADER)?.toLowerCase() === 'external'; +} export const sendPhoneOTP = async (req: Request, res: Response) => { const authReq = req as AuthenticatedRequest; const user = authReq.user; const phone = user.phone; + const normalizedPhone = normalizePhoneNumber(phone); + const useExternalDelivery = wantsExternalDelivery(req); if (!phone) { logger.warn(`Missing phone`); @@ -42,7 +49,7 @@ export const sendPhoneOTP = async (req: Request, res: Response) => { logger.info(`Sending OTP to phone number: ${phone}`); try { - if (!isValidPhoneNumber(phone)) { + if (!isValidPhoneNumber(phone) || !normalizedPhone) { logger.warn(`Invalid phone provided: ${phone}`); AuthEventService.log({ userId: null, @@ -65,7 +72,9 @@ export const sendPhoneOTP = async (req: Request, res: Response) => { } logger.info(`${phone} requested a phone OTP`); - await generatePhoneOTP(user); + const generatedToken = await generatePhoneOTP(user, { + sendMessage: !useExternalDelivery, + }); AuthEventService.log({ userId: null, @@ -77,10 +86,33 @@ export const sendPhoneOTP = async (req: Request, res: Response) => { if (AUTH_MODE === 'web') { await setAuthCookies(res, { ephemeralToken: token }); - return res.status(200).json({ message: 'success' }); + return res.status(200).json({ + message: 'success', + ...(useExternalDelivery + ? { + delivery: { + kind: 'otp_sms', + to: normalizedPhone, + token: generatedToken, + }, + } + : {}), + }); } - return res.status(200).json({ message: 'success', token }); + return res.status(200).json({ + message: 'success', + token, + ...(useExternalDelivery + ? { + delivery: { + kind: 'otp_sms', + to: normalizedPhone, + token: generatedToken, + }, + } + : {}), + }); } catch (error: unknown) { if (error instanceof Error) { logger.error(`Error sending phone OTP ${error.message}`); @@ -96,6 +128,7 @@ export const sendEmailOTP = async (req: Request, res: Response) => { const authReq = req as AuthenticatedRequest; const user = authReq.user; const email = user.email; + const useExternalDelivery = wantsExternalDelivery(req); try { if (!user) { @@ -134,7 +167,9 @@ export const sendEmailOTP = async (req: Request, res: Response) => { } logger.info(`${email} requested an email OTP`); - await generateEmailOTP(user); + const generatedToken = await generateEmailOTP(user, { + sendMessage: !useExternalDelivery, + }); AuthEventService.log({ userId: null, type: 'otp_success', @@ -145,10 +180,33 @@ export const sendEmailOTP = async (req: Request, res: Response) => { if (AUTH_MODE === 'web') { await setAuthCookies(res, { ephemeralToken: token }); - return res.status(200).json({ message: 'success' }); + return res.status(200).json({ + message: 'success', + ...(useExternalDelivery + ? { + delivery: { + kind: 'otp_email', + to: email, + token: generatedToken, + }, + } + : {}), + }); } - return res.status(200).json({ message: 'success', token }); + return res.status(200).json({ + message: 'success', + token, + ...(useExternalDelivery + ? { + delivery: { + kind: 'otp_email', + to: email, + token: generatedToken, + }, + } + : {}), + }); } catch (error: unknown) { if (error instanceof Error) { logger.error(`Error sending email OTP ${error.message}`); diff --git a/src/controllers/registration.ts b/src/controllers/registration.ts index fd475fd..5a6afec 100644 --- a/src/controllers/registration.ts +++ b/src/controllers/registration.ts @@ -5,7 +5,6 @@ */ import { Request, Response } from 'express'; -import { Op } from 'sequelize'; import { getSystemConfig } from '../config/getSystemConfig.js'; import { setBootstrapCookie } from '../lib/bootstrapCookie.js'; @@ -16,13 +15,21 @@ import { User } from '../models/users.js'; import { AuthEventService } from '../services/authEventService.js'; import getLogger from '../utils/logger.js'; import { generatePhoneOTP } from '../utils/otp.js'; -import { isValidEmail, isValidPhoneNumber } from '../utils/utils.js'; +import { isValidEmail, isValidPhoneNumber, normalizePhoneNumber } from '../utils/utils.js'; const logger = getLogger('registration'); const AUTH_MODE = process.env.AUTH_MODE; +const EXTERNAL_DELIVERY_HEADER = 'x-seamless-auth-delivery-mode'; + +function wantsExternalDelivery(req: Request) { + return req.get(EXTERNAL_DELIVERY_HEADER)?.toLowerCase() === 'external'; +} export const register = async (req: Request, res: Response) => { const { email, phone, bootstrapToken } = req.body; + const useExternalDelivery = wantsExternalDelivery(req); + const normalizedEmail = email?.toLowerCase(); + const normalizedPhone = typeof phone === 'string' ? normalizePhoneNumber(phone) : null; if (bootstrapToken && bootstrapToken.length > 10) { setBootstrapCookie(res, bootstrapToken); @@ -34,9 +41,9 @@ export const register = async (req: Request, res: Response) => { logger.info(`Registering phone and email account`); try { - if (!isValidEmail(email) || !isValidPhoneNumber(phone)) { + if (!isValidEmail(email) || !isValidPhoneNumber(phone) || !normalizedPhone) { logger.error(`Invalid email or phone provided: ${email} - ${phone}`); - AuthEventService.log({ + await AuthEventService.log({ userId: null, type: 'registration_suspicious', req, @@ -46,21 +53,47 @@ export const register = async (req: Request, res: Response) => { return res.status(400).json({ message: 'Invalid data.' }); } - const now = new Date(); - now.setMinutes(now.getMinutes() + 5); + const [existingEmailUser, existingPhoneUser] = await Promise.all([ + User.findOne({ where: { email: normalizedEmail } }), + User.findOne({ where: { phone: normalizedPhone } }), + ]); + + const hasExactExistingUser = + existingEmailUser && existingPhoneUser && existingEmailUser.id === existingPhoneUser.id; + const hasIdentifierConflict = + (existingEmailUser && !existingPhoneUser) || + (!existingEmailUser && existingPhoneUser) || + (existingEmailUser && existingPhoneUser && existingEmailUser.id !== existingPhoneUser.id); + + if (hasIdentifierConflict) { + logger.warn(`Registration conflict for email ${normalizedEmail} and phone ${phone}`); + await AuthEventService.log({ + userId: existingEmailUser?.id ?? existingPhoneUser?.id ?? null, + type: 'registration_suspicious', + req, + metadata: { + reason: 'Registration attempted with mismatched existing identifiers.', + emailInUse: Boolean(existingEmailUser), + phoneInUse: Boolean(existingPhoneUser), + }, + }); - let user = await User.findOne({ - where: { - [Op.or]: [{ email: email.toLowerCase() }, { phone: phone }], - }, - }); + return res.status(409).json({ + error: 'Registration conflict', + message: + 'The provided email and phone do not belong to the same account. Try signing in with your existing account details or use a different email and phone.', + }); + } + + let user = hasExactExistingUser ? existingEmailUser : null; let token; + let phoneOtp: number | null = null; if (user) { logger.info(`Registration attempt for a user that already exisited`); logger.info(`Sending OTP`); - AuthEventService.log({ + await AuthEventService.log({ userId: user.id, type: 'informational', req, @@ -69,17 +102,19 @@ export const register = async (req: Request, res: Response) => { token = await signEphemeralToken(user.id); - await generatePhoneOTP(user); + phoneOtp = await generatePhoneOTP(user, { + sendMessage: !useExternalDelivery, + }); } else { logger.info(`Creating new user`); user = await User.create({ - email: email.toLowerCase(), - phone, + email: normalizedEmail, + phone: normalizedPhone, roles: systemConfig.default_roles, }); - AuthEventService.log({ + await AuthEventService.log({ userId: user.id, type: 'user_created', req, @@ -88,14 +123,16 @@ export const register = async (req: Request, res: Response) => { token = await signEphemeralToken(user.id); - AuthEventService.notificationSent(user.id, req, { + await AuthEventService.notificationSent(user.id, req, { reason: 'Owner notified of new user registration', }); - logger.info(`Sending phone OTP to ${phone}`); - await generatePhoneOTP(user); + logger.info(`Sending phone OTP to ${normalizedPhone}`); + phoneOtp = await generatePhoneOTP(user, { + sendMessage: !useExternalDelivery, + }); - AuthEventService.log({ + await AuthEventService.log({ userId: user.id, type: 'registration_success', req, @@ -103,13 +140,31 @@ export const register = async (req: Request, res: Response) => { }); } + const delivery = + useExternalDelivery && phoneOtp !== null + ? { + kind: 'otp_sms', + to: normalizePhoneNumber(user.phone) ?? user.phone, + token: phoneOtp, + } + : undefined; + if (AUTH_MODE === 'web') { await setAuthCookies(res, { ephemeralToken: token }); - res.status(200).json({ message: 'Success' }); + res.status(200).json({ + message: 'Success', + ...(delivery ? { delivery } : {}), + }); return; } - return res.status(200).json({ message: 'Success', sub: user.id, token, ttl: '300' }); + return res.status(200).json({ + message: 'Success', + sub: user.id, + token, + ttl: '300', + ...(delivery ? { delivery } : {}), + }); } catch (error: unknown) { if (error instanceof Error) { logger.error(`Error during registration for email ${email}: ${error}`); diff --git a/src/lib/defineRoute.ts b/src/lib/defineRoute.ts index 46ee861..e61f3e1 100644 --- a/src/lib/defineRoute.ts +++ b/src/lib/defineRoute.ts @@ -8,6 +8,10 @@ import { NextFunction, RequestHandler, Response, Router } from 'express'; import { ZodError, ZodTypeAny } from 'zod'; import { attachAuthMiddleware } from '../middleware/attachAuthMiddleware.js'; +import { + AuthAwareRequestHandler, + getSecuritySchemeName, +} from '../middleware/attachAuthMiddleware.js'; import { registry } from '../openapi/registry.js'; import { CookieType } from '../services/sessionService.js'; import getLogger from '../utils/logger.js'; @@ -90,11 +94,31 @@ function buildResponses( return responses; } +function resolveAuthType( + auth: CookieType | undefined, + middleware: RequestHandler[] | undefined, +): CookieType | undefined { + if (auth) { + return auth; + } + + for (const handler of middleware ?? []) { + const authType = (handler as AuthAwareRequestHandler).seamlessAuthType; + + if (authType) { + return authType; + } + } + + return undefined; +} + export function defineRoute( router: Router, options: DefineRouteOptions, ): void { const { method, path, auth, schemas, summary, description, tags, handler } = options; + const authType = resolveAuthType(auth, options.middleware); const params = schemas?.params; const query = schemas?.query; @@ -109,7 +133,7 @@ export function defineRoute( summary, description, tags, - security: auth ? [{ bearerAuth: [] }] : undefined, + security: authType ? [{ [getSecuritySchemeName(authType)]: [] }] : undefined, request: { params, query, @@ -186,14 +210,14 @@ export function defineRoute( const middlewareStack: RequestHandler[] = []; - if (options.middleware) { - middlewareStack.push(...options.middleware); - } - if (auth) { middlewareStack.push(attachAuthMiddleware(auth)); } + if (options.middleware) { + middlewareStack.push(...options.middleware); + } + middlewareStack.push(validate); middlewareStack.push(wrappedHandler); diff --git a/src/lib/token.ts b/src/lib/token.ts index c14d2a0..3e89219 100644 --- a/src/lib/token.ts +++ b/src/lib/token.ts @@ -5,7 +5,7 @@ */ import { hashSync } from 'bcrypt-ts'; -import { randomBytes } from 'crypto'; +import { createHmac, randomBytes } from 'crypto'; import { importPKCS8, SignJWT } from 'jose'; import { getSystemConfig } from '../config/getSystemConfig.js'; @@ -15,6 +15,34 @@ import { getSigningKey } from '../utils/signingKeyStore.js'; const logger = getLogger('tokens'); const ISSUER = process.env.ISSUER!; +let warnedAboutDevLookupSecret = false; + +function getRefreshTokenLookupSecret() { + const explicitSecret = process.env.REFRESH_TOKEN_LOOKUP_SECRET?.trim(); + if (explicitSecret) { + return explicitSecret; + } + + const apiServiceSecret = process.env.API_SERVICE_TOKEN?.trim(); + if (apiServiceSecret) { + return apiServiceSecret; + } + + if (process.env.NODE_ENV !== 'production') { + if (!warnedAboutDevLookupSecret) { + logger.warn( + 'REFRESH_TOKEN_LOOKUP_SECRET is not set. Falling back to a development-only secret for refresh token lookup fingerprints.', + ); + warnedAboutDevLookupSecret = true; + } + + return `dev-refresh-lookup:${process.env.APP_ID ?? 'local'}:${ISSUER}`; + } + + throw new Error( + 'REFRESH_TOKEN_LOOKUP_SECRET (or API_SERVICE_TOKEN) must be set to derive refresh token lookup fingerprints in production.', + ); +} export async function signAccessToken(sessionId: string, userId: string, roles?: string[]) { const { kid, privateKeyPem } = await getSigningKey(); @@ -91,3 +119,7 @@ export async function hashRefreshToken(token: string) { const saltRounds = 12; return hashSync(token, saltRounds); } + +export function createRefreshTokenLookup(token: string) { + return createHmac('sha256', getRefreshTokenLookupSecret()).update(token).digest('hex'); +} diff --git a/src/middleware/attachAuthMiddleware.ts b/src/middleware/attachAuthMiddleware.ts index bfa744b..acd6344 100644 --- a/src/middleware/attachAuthMiddleware.ts +++ b/src/middleware/attachAuthMiddleware.ts @@ -4,11 +4,33 @@ * See LICENSE file in the project root for full license information */ +import { RequestHandler } from 'express'; + import { CookieType } from '../services/sessionService.js'; import { verifyBearerAuth } from './verifyBearerAuth.js'; import { verifyCookieAuth } from './verifyCookieAuth.js'; +export type AuthAwareRequestHandler = RequestHandler & { + seamlessAuthType?: CookieType; +}; + +export function getSecuritySchemeName(cookieType: CookieType): string { + const mode = (process.env.AUTH_MODE || 'web').toLowerCase(); + + if (mode === 'server') { + return 'bearerAuth'; + } + + return cookieType === 'ephemeral' ? 'ephemeralCookieAuth' : 'accessCookieAuth'; +} + export function attachAuthMiddleware(cookieType: CookieType = 'access') { const mode = (process.env.AUTH_MODE || 'web').toLowerCase(); - return mode === 'server' ? verifyBearerAuth : verifyCookieAuth(cookieType); + const handler = ( + mode === 'server' ? verifyBearerAuth : verifyCookieAuth(cookieType) + ) as AuthAwareRequestHandler; + + handler.seamlessAuthType = cookieType; + + return handler; } diff --git a/src/middleware/verifyCookieAuth.ts b/src/middleware/verifyCookieAuth.ts index 909237b..0b60830 100644 --- a/src/middleware/verifyCookieAuth.ts +++ b/src/middleware/verifyCookieAuth.ts @@ -4,17 +4,21 @@ * See LICENSE file in the project root for full license information */ -import { compareSync } from 'bcrypt-ts'; import { NextFunction, Request, Response } from 'express'; -import { Op } from 'sequelize'; import { clearAuthCookies, setAuthCookies } from '../lib/cookie.js'; -import { generateRefreshToken, hashRefreshToken, signAccessToken } from '../lib/token.js'; +import { + createRefreshTokenLookup, + generateRefreshToken, + hashRefreshToken, + signAccessToken, +} from '../lib/token.js'; import { Session } from '../models/sessions.js'; import { User } from '../models/users.js'; import { AuthEventService } from '../services/authEventService.js'; import { CookieType, + findRefreshSessionByToken, getUserFromSession, hardRevokeSession, revokeSessionChain, @@ -42,7 +46,10 @@ export function verifyCookieAuth(cookieType: CookieType = 'access') { } const payload = await verifyJwtWithKid(ephemeralCookie, cookieType); - if (!payload) return null; + if (!payload) { + clearAuthCookies(res); + return res.status(401).json({ error: 'unauthorized' }); + } const user = await User.findOne({ where: { id: payload.sub, revoked: false }, @@ -112,31 +119,25 @@ async function performSilentRefresh(req: Request, res: Response): Promise { type: DataTypes.TEXT, allowNull: false, }, + refreshTokenLookup: { + type: DataTypes.STRING(64), + allowNull: true, + }, userAgent: DataTypes.TEXT, ipAddress: DataTypes.STRING, deviceName: DataTypes.STRING, diff --git a/src/openapi/document.ts b/src/openapi/document.ts index d9d33f2..a104d60 100644 --- a/src/openapi/document.ts +++ b/src/openapi/document.ts @@ -35,10 +35,15 @@ export function generateOpenApiDocument() { scheme: 'bearer', bearerFormat: 'JWT', }, - cookieAuth: { + accessCookieAuth: { type: 'apiKey', in: 'cookie', - name: 'access_token', + name: 'seamless_access', + }, + ephemeralCookieAuth: { + type: 'apiKey', + in: 'cookie', + name: 'seamless_ephemeral', }, }, }; diff --git a/src/routes/admin.routes.ts b/src/routes/admin.routes.ts index e842823..5d9579a 100644 --- a/src/routes/admin.routes.ts +++ b/src/routes/admin.routes.ts @@ -20,7 +20,6 @@ import { updateUser, } from '../controllers/admin.js'; import { createRouter } from '../lib/createRouter.js'; -import { attachAuthMiddleware } from '../middleware/attachAuthMiddleware.js'; import { requireAdmin } from '../middleware/requireAdmin.js'; import { UserIdParamSchema } from '../schemas/admin.query.js'; import { UserResponseSchema } from '../schemas/admin.responses.js'; @@ -38,9 +37,10 @@ const adminRouter = createRouter('/admin'); adminRouter.get( '/users', { + auth: 'access', summary: 'List users (internal)', tags: ['Admin'], - middleware: [attachAuthMiddleware(), requireAdmin()], + middleware: [requireAdmin()], schemas: { response: { @@ -55,7 +55,8 @@ adminRouter.get( adminRouter.get( '/auth-events', { - middleware: [attachAuthMiddleware(), requireAdmin()], + auth: 'access', + middleware: [requireAdmin()], tags: ['Admin'], schemas: { query: AuthEventQuerySchema, @@ -70,9 +71,10 @@ adminRouter.get( adminRouter.get( '/credential-count', { + auth: 'access', summary: 'Get credential count', tags: ['Admin'], - middleware: [attachAuthMiddleware(), requireAdmin()], + middleware: [requireAdmin()], schemas: { response: { @@ -87,8 +89,9 @@ adminRouter.get( adminRouter.post( '/users', { + auth: 'access', tags: ['Admin'], - middleware: [attachAuthMiddleware(), requireAdmin()], + middleware: [requireAdmin()], schemas: { body: CreateUserSchema, }, @@ -99,9 +102,10 @@ adminRouter.post( adminRouter.delete( '/users', { + auth: 'access', summary: 'Delete user', tags: ['Admin'], - middleware: [attachAuthMiddleware(), requireAdmin()], + middleware: [requireAdmin()], schemas: { response: { @@ -116,9 +120,10 @@ adminRouter.delete( adminRouter.patch( '/users/:userId', { + auth: 'access', summary: 'Update user', tags: ['Admin'], - middleware: [attachAuthMiddleware(), requireAdmin()], + middleware: [requireAdmin()], schemas: { body: UpdateUserSchema, @@ -136,8 +141,9 @@ adminRouter.patch( adminRouter.get( '/users/:userId', { + auth: 'access', tags: ['Admin'], - middleware: [attachAuthMiddleware(), requireAdmin()], + middleware: [requireAdmin()], }, getUserDetail, ); @@ -145,8 +151,9 @@ adminRouter.get( adminRouter.get( '/users/:userId/anomalies', { + auth: 'access', tags: ['Admin'], - middleware: [attachAuthMiddleware(), requireAdmin()], + middleware: [requireAdmin()], }, getUserAnomalies, ); @@ -154,9 +161,10 @@ adminRouter.get( adminRouter.get( '/sessions', { + auth: 'access', tags: ['Admin'], - middleware: [attachAuthMiddleware(), requireAdmin()], - schema: { + middleware: [requireAdmin()], + schemas: { query: PaginationQuerySchema, }, }, @@ -166,7 +174,8 @@ adminRouter.get( adminRouter.get( '/sessions/:userId', { - middleware: [attachAuthMiddleware(), requireAdmin()], + auth: 'access', + middleware: [requireAdmin()], tags: ['Admin'], schemas: { params: UserIdParamSchema, @@ -182,7 +191,8 @@ adminRouter.get( adminRouter.delete( '/sessions/:userId/revoke-all', { - middleware: [attachAuthMiddleware(), requireAdmin()], + auth: 'access', + middleware: [requireAdmin()], tags: ['Admin'], schemas: { params: UserIdParamSchema, diff --git a/src/routes/auth.routes.ts b/src/routes/auth.routes.ts index 5cd9071..2235271 100644 --- a/src/routes/auth.routes.ts +++ b/src/routes/auth.routes.ts @@ -6,9 +6,11 @@ import { login, logout, refreshSession } from '../controllers/authentication.js'; import { createRouter } from '../lib/createRouter.js'; -import { attachAuthMiddleware } from '../middleware/attachAuthMiddleware.js'; import { LoginRequestSchema } from '../schemas/auth.requests.js'; -import { LoginSuccessResponseSchema } from '../schemas/auth.responses.js'; +import { + LoginSuccessResponseSchema, + RefreshSuccessResponseSchema, +} from '../schemas/auth.responses.js'; import { ErrorSchema, InternalErrorSchema, MessageSchema } from '../schemas/generic.responses.js'; const authRouter = createRouter(''); @@ -37,9 +39,9 @@ authRouter.post( authRouter.get( '/logout', { + auth: 'access', summary: 'Logout current user', tags: ['Authentication'], - middleware: [attachAuthMiddleware('access')], schemas: { response: { @@ -55,11 +57,10 @@ authRouter.post( { summary: 'Refresh access token', tags: ['Authentication'], - middleware: [attachAuthMiddleware('access')], schemas: { response: { - 200: MessageSchema, + 200: RefreshSuccessResponseSchema, 401: ErrorSchema, 500: InternalErrorSchema, }, @@ -68,4 +69,9 @@ authRouter.post( refreshSession, ); +authRouter.router.all('/refresh', (_req, res) => { + res.setHeader('Allow', 'POST'); + return res.status(405).json({ error: 'Method Not Allowed' }); +}); + export default authRouter.router; diff --git a/src/routes/internal.routes.ts b/src/routes/internal.routes.ts index f03ea19..01b68d0 100644 --- a/src/routes/internal.routes.ts +++ b/src/routes/internal.routes.ts @@ -13,7 +13,6 @@ import { } from '../controllers/internalMetrics.js'; import { getSecurityAnomalies } from '../controllers/internalSecurity.js'; import { createRouter } from '../lib/createRouter.js'; -import { attachAuthMiddleware } from '../middleware/attachAuthMiddleware.js'; import { requireAdmin } from '../middleware/requireAdmin.js'; import { MetricsQuerySchema } from '../schemas/internal.query.js'; @@ -22,7 +21,8 @@ const internalRouter = createRouter('/internal'); internalRouter.get( '/auth-events/summary', { - middleware: [attachAuthMiddleware(), requireAdmin()], + auth: 'access', + middleware: [requireAdmin()], tags: ['Internal'], schemas: { query: MetricsQuerySchema, @@ -34,7 +34,8 @@ internalRouter.get( internalRouter.get( '/auth-events/timeseries', { - middleware: [attachAuthMiddleware(), requireAdmin()], + auth: 'access', + middleware: [requireAdmin()], tags: ['Internal'], schemas: { query: MetricsQuerySchema, @@ -46,7 +47,8 @@ internalRouter.get( internalRouter.get( '/auth-events/login-stats', { - middleware: [attachAuthMiddleware(), requireAdmin()], + auth: 'access', + middleware: [requireAdmin()], tags: ['Internal'], }, getLoginStats, @@ -55,7 +57,8 @@ internalRouter.get( internalRouter.get( '/security/anomalies', { - middleware: [attachAuthMiddleware(), requireAdmin()], + auth: 'access', + middleware: [requireAdmin()], summary: 'Detect suspicious activity', tags: ['Internal'], }, @@ -65,7 +68,8 @@ internalRouter.get( internalRouter.get( '/metrics/dashboard', { - middleware: [attachAuthMiddleware(), requireAdmin()], + auth: 'access', + middleware: [requireAdmin()], summary: 'Dashboard metrics', tags: ['Internal'], }, @@ -75,7 +79,8 @@ internalRouter.get( internalRouter.get( '/auth-events/grouped', { - middleware: [attachAuthMiddleware(), requireAdmin()], + auth: 'access', + middleware: [requireAdmin()], summary: 'Auth Event metrics grouped', tags: ['Internal'], }, diff --git a/src/routes/magicLink.routes.ts b/src/routes/magicLink.routes.ts index e97fc00..c68f0d6 100644 --- a/src/routes/magicLink.routes.ts +++ b/src/routes/magicLink.routes.ts @@ -10,7 +10,6 @@ import { verifyMagicLink, } from '../controllers/magicLinks.js'; import { createRouter } from '../lib/createRouter.js'; -import { attachAuthMiddleware } from '../middleware/attachAuthMiddleware.js'; import { magicLinkEmailLimiter, magicLinkIpLimiter } from '../middleware/rateLimit.js'; import { ErrorSchema, InternalErrorSchema, MessageSchema } from '../schemas/generic.responses.js'; import { MagicLinkVerifyParamsSchema } from '../schemas/magiclink.requests.js'; @@ -21,9 +20,10 @@ const magicLinkRouter = createRouter('/magic-link'); magicLinkRouter.get( '', { + auth: 'ephemeral', summary: 'Request a magic login link', tags: ['MagicLinks'], - middleware: [attachAuthMiddleware('ephemeral'), magicLinkIpLimiter, magicLinkEmailLimiter], + middleware: [magicLinkIpLimiter, magicLinkEmailLimiter], schemas: { response: { @@ -37,9 +37,9 @@ magicLinkRouter.get( magicLinkRouter.get( '/check', { + auth: 'ephemeral', summary: 'Poll for magic link confirmation', tags: ['MagicLinks'], - middleware: [attachAuthMiddleware('ephemeral')], schemas: { response: { diff --git a/src/routes/otp.routes.ts b/src/routes/otp.routes.ts index cdfbd2c..624f9e4 100644 --- a/src/routes/otp.routes.ts +++ b/src/routes/otp.routes.ts @@ -13,7 +13,6 @@ import { verifyPhoneNumber, } from '../controllers/otp.js'; import { createRouter } from '../lib/createRouter.js'; -import { attachAuthMiddleware } from '../middleware/attachAuthMiddleware.js'; import { ErrorSchema, InternalErrorSchema, MessageSchema } from '../schemas/generic.responses.js'; import { VerifyOTPRequestSchema } from '../schemas/otp.requests.js'; import { OTPVerifyTokenSuccessSchema } from '../schemas/otp.responses.js'; @@ -23,9 +22,9 @@ const otpRouter = createRouter('/otp'); otpRouter.get( '/generate-email-otp', { + auth: 'ephemeral', summary: 'Generate email OTP', tags: ['OTP'], - middleware: [attachAuthMiddleware('ephemeral')], schemas: { response: { @@ -41,9 +40,9 @@ otpRouter.get( otpRouter.get( '/generate-phone-otp', { + auth: 'ephemeral', summary: 'Generate phone OTP', tags: ['OTP'], - middleware: [attachAuthMiddleware('ephemeral')], schemas: { response: { @@ -59,9 +58,9 @@ otpRouter.get( otpRouter.get( '/generate-login-email-otp', { + auth: 'ephemeral', summary: 'Generate login email OTP', tags: ['OTP'], - middleware: [attachAuthMiddleware('ephemeral')], schemas: { response: { @@ -75,9 +74,9 @@ otpRouter.get( otpRouter.get( '/generate-login-phone-otp', { + auth: 'ephemeral', summary: 'Generate login phone OTP', tags: ['OTP'], - middleware: [attachAuthMiddleware('ephemeral')], schemas: { response: { @@ -91,9 +90,9 @@ otpRouter.get( otpRouter.post( '/verify-email-otp', { + auth: 'ephemeral', summary: 'Verify email OTP', tags: ['OTP'], - middleware: [attachAuthMiddleware('ephemeral')], schemas: { body: VerifyOTPRequestSchema, @@ -111,9 +110,9 @@ otpRouter.post( otpRouter.post( '/verify-phone-otp', { + auth: 'ephemeral', summary: 'Verify phone OTP', tags: ['OTP'], - middleware: [attachAuthMiddleware('ephemeral')], schemas: { body: VerifyOTPRequestSchema, @@ -131,9 +130,9 @@ otpRouter.post( otpRouter.post( '/verify-login-email-otp', { + auth: 'ephemeral', summary: 'Verify login email OTP', tags: ['OTP'], - middleware: [attachAuthMiddleware('ephemeral')], schemas: { body: VerifyOTPRequestSchema, @@ -151,9 +150,9 @@ otpRouter.post( otpRouter.post( '/verify-login-phone-otp', { + auth: 'ephemeral', summary: 'Verify login phone OTP', tags: ['OTP'], - middleware: [attachAuthMiddleware('ephemeral')], schemas: { body: VerifyOTPRequestSchema, diff --git a/src/routes/registration.routes.ts b/src/routes/registration.routes.ts index 68469f7..d0befe4 100644 --- a/src/routes/registration.routes.ts +++ b/src/routes/registration.routes.ts @@ -24,6 +24,7 @@ registrationRouter.post( response: { 200: RegistrationSuccessSchema, 400: ErrorSchema, + 409: ErrorSchema, 500: ErrorSchema, }, }, diff --git a/src/routes/session.routes.ts b/src/routes/session.routes.ts index 4134af8..6ad6fa6 100644 --- a/src/routes/session.routes.ts +++ b/src/routes/session.routes.ts @@ -6,7 +6,6 @@ import { listSessions, revokeAllSessions, revokeSession } from '../controllers/sessions.js'; import { createRouter } from '../lib/createRouter.js'; -import { attachAuthMiddleware } from '../middleware/attachAuthMiddleware.js'; import { ErrorSchema, MessageSchema } from '../schemas/generic.responses.js'; import { SessionIdParamsSchema } from '../schemas/session.params.js'; import { SessionListResponseSchema } from '../schemas/session.responses.js'; @@ -16,9 +15,9 @@ const sessionsRouter = createRouter('/sessions'); sessionsRouter.get( '', { + auth: 'access', summary: 'List active sessions', tags: ['Sessions'], - middleware: [attachAuthMiddleware('access')], schemas: { response: { @@ -33,9 +32,9 @@ sessionsRouter.get( sessionsRouter.delete( '/:id', { + auth: 'access', summary: 'Revoke a session', tags: ['Sessions'], - middleware: [attachAuthMiddleware('access')], schemas: { params: SessionIdParamsSchema, @@ -53,9 +52,9 @@ sessionsRouter.delete( sessionsRouter.delete( '', { + auth: 'access', summary: 'Revoke all sessions', tags: ['Sessions'], - middleware: [attachAuthMiddleware('access')], schemas: { response: { diff --git a/src/routes/systemConfig.routes.ts b/src/routes/systemConfig.routes.ts index 8e32240..13efa26 100644 --- a/src/routes/systemConfig.routes.ts +++ b/src/routes/systemConfig.routes.ts @@ -10,7 +10,6 @@ import { updateSystemConfig, } from '../controllers/systemConfig.js'; import { createRouter } from '../lib/createRouter.js'; -import { attachAuthMiddleware } from '../middleware/attachAuthMiddleware.js'; import { requireAdmin } from '../middleware/requireAdmin.js'; import { ErrorSchema, InternalErrorSchema } from '../schemas/generic.responses.js'; import { @@ -24,10 +23,11 @@ const systemConfigRouter = createRouter('/system-config'); systemConfigRouter.get( '/roles', { + auth: 'access', summary: 'Get available roles', tags: ['SystemConfig'], - middleware: [attachAuthMiddleware(), requireAdmin()], + middleware: [requireAdmin()], }, getAvailableRoles, ); @@ -35,10 +35,11 @@ systemConfigRouter.get( systemConfigRouter.get( '/admin', { + auth: 'access', summary: 'Retrieve system configuration', tags: ['SystemConfig'], - middleware: [attachAuthMiddleware(), requireAdmin()], + middleware: [requireAdmin()], schemas: { response: { @@ -54,10 +55,11 @@ systemConfigRouter.get( systemConfigRouter.patch( '/admin', { + auth: 'access', summary: 'Update system configuration', tags: ['SystemConfig'], - middleware: [attachAuthMiddleware(), requireAdmin()], + middleware: [requireAdmin()], schemas: { response: { diff --git a/src/routes/users.routes.ts b/src/routes/users.routes.ts index 8a010c5..ed14080 100644 --- a/src/routes/users.routes.ts +++ b/src/routes/users.routes.ts @@ -8,7 +8,6 @@ import { DeleteCredentialRequestSchema, UpdateCredentialRequestSchema } from '@s import { deleteCredential, deleteUser, getUser, updateCredential } from '../controllers/user.js'; import { createRouter } from '../lib/createRouter.js'; -import { attachAuthMiddleware } from '../middleware/attachAuthMiddleware.js'; import { MessageSchema } from '../schemas/generic.responses.js'; import { MeResponseSchema } from '../schemas/me.response.js'; @@ -34,8 +33,6 @@ usersRouter.post( tags: ['Users'], summary: 'Update credential metadata', - middleware: [attachAuthMiddleware('access')], - schemas: { body: UpdateCredentialRequestSchema, }, @@ -50,8 +47,6 @@ usersRouter.delete( tags: ['Users'], summary: 'Delete authenticated user', - middleware: [attachAuthMiddleware('access')], - schemas: { response: MessageSchema, }, @@ -66,8 +61,6 @@ usersRouter.delete( tags: ['Users'], summary: 'Delete credential', - middleware: [attachAuthMiddleware('access')], - schemas: { body: DeleteCredentialRequestSchema, }, diff --git a/src/routes/webauthn.routes.ts b/src/routes/webauthn.routes.ts index cbca778..37f7464 100644 --- a/src/routes/webauthn.routes.ts +++ b/src/routes/webauthn.routes.ts @@ -11,7 +11,6 @@ import { verifyWebAuthnRegistration, } from '../controllers/webauthn.js'; import { createRouter } from '../lib/createRouter.js'; -import { attachAuthMiddleware } from '../middleware/attachAuthMiddleware.js'; import { ErrorSchema, InternalErrorSchema } from '../schemas/generic.responses.js'; import { WebAuthnLoginFinishSchema, @@ -27,9 +26,9 @@ const webauthnRouter = createRouter('/webauthn'); webauthnRouter.get( '/register/start', { + auth: 'ephemeral', summary: 'Start WebAuthn registration', tags: ['WebAuthn'], - middleware: [attachAuthMiddleware('ephemeral')], schemas: { response: { @@ -45,9 +44,9 @@ webauthnRouter.get( webauthnRouter.post( '/register/finish', { + auth: 'ephemeral', summary: 'Finish WebAuthn registration', tags: ['WebAuthn'], - middleware: [attachAuthMiddleware('ephemeral')], schemas: { body: WebAuthnRegisterFinishSchema, @@ -65,9 +64,9 @@ webauthnRouter.post( webauthnRouter.post( '/login/start', { + auth: 'ephemeral', summary: 'Start WebAuthn login', tags: ['WebAuthn'], - middleware: [attachAuthMiddleware('ephemeral')], schemas: { response: { @@ -84,9 +83,9 @@ webauthnRouter.post( webauthnRouter.post( '/login/finish', { + auth: 'ephemeral', summary: 'Finish WebAuthn login', tags: ['WebAuthn'], - middleware: [attachAuthMiddleware('ephemeral')], schemas: { body: WebAuthnLoginFinishSchema, diff --git a/src/schemas/auth.responses.ts b/src/schemas/auth.responses.ts index 5d2ed4a..5c292c5 100644 --- a/src/schemas/auth.responses.ts +++ b/src/schemas/auth.responses.ts @@ -19,6 +19,9 @@ export const RefreshSuccessResponseSchema = z.object({ token: z.string().optional(), refreshToken: z.string().optional(), sub: z.string().optional(), + roles: z.array(z.string()).optional(), + email: z.string().optional(), + phone: z.string().nullable().optional(), ttl: z.number().optional(), refreshTtl: z.number().optional(), }); diff --git a/src/schemas/bootstrap.schema.ts b/src/schemas/bootstrap.schema.ts index 272d1f1..6be5bd4 100644 --- a/src/schemas/bootstrap.schema.ts +++ b/src/schemas/bootstrap.schema.ts @@ -6,6 +6,8 @@ import { z } from 'zod'; +import { AuthDeliverySchema } from './generic.responses.js'; + export const BootstrapAdminInviteBodySchema = z.object({ email: z.email().max(320), }); @@ -16,6 +18,7 @@ export const BootstrapAdminInviteResponseSchema = z.object({ url: z.url(), expiresAt: z.iso.datetime(), token: z.string().min(32), + delivery: AuthDeliverySchema.optional(), }), }); diff --git a/src/schemas/generic.responses.ts b/src/schemas/generic.responses.ts index b49e85d..44ef52f 100644 --- a/src/schemas/generic.responses.ts +++ b/src/schemas/generic.responses.ts @@ -6,8 +6,34 @@ import z from 'zod'; +export const AuthDeliverySchema = z.discriminatedUnion('kind', [ + z.object({ + kind: z.literal('otp_sms'), + to: z.string(), + token: z.union([z.string(), z.number()]), + }), + z.object({ + kind: z.literal('otp_email'), + to: z.string(), + token: z.string(), + }), + z.object({ + kind: z.literal('magic_link_email'), + to: z.string(), + token: z.string(), + magicLinkUrl: z.string(), + }), + z.object({ + kind: z.literal('bootstrap_invite_email'), + to: z.string(), + token: z.string(), + inviteUrl: z.string(), + }), +]); + export const MessageSchema = z.object({ message: z.string(), + delivery: AuthDeliverySchema.optional(), }); export const ErrorSchema = z.object({ diff --git a/src/schemas/registration.responses.ts b/src/schemas/registration.responses.ts index b9d01b5..cb3e73d 100644 --- a/src/schemas/registration.responses.ts +++ b/src/schemas/registration.responses.ts @@ -6,9 +6,12 @@ import { z } from 'zod'; +import { AuthDeliverySchema } from './generic.responses.js'; + export const RegistrationSuccessSchema = z.object({ message: z.string(), sub: z.string().optional(), token: z.string().optional(), ttl: z.string().optional(), + delivery: AuthDeliverySchema.optional(), }); diff --git a/src/services/bootstrapService.ts b/src/services/bootstrapService.ts index a92bdf0..6b78788 100644 --- a/src/services/bootstrapService.ts +++ b/src/services/bootstrapService.ts @@ -92,6 +92,7 @@ export async function createAdminBootstrapInvite(params: { email: string; createdIp?: string | null; createdUserAgent?: string | null; + sendMessage?: boolean; }) { await assertBootstrapAllowed(); @@ -138,9 +139,12 @@ export async function createAdminBootstrapInvite(params: { logger.info('invite link: ', registrationUrl); } - sendBootstrapEmail(params.email, registrationUrl); + if (params.sendMessage !== false) { + await sendBootstrapEmail(params.email, registrationUrl); + } return { + email: params.email.toLowerCase(), registrationUrl, token: rawToken, expiresAt, diff --git a/src/services/messagingService.ts b/src/services/messagingService.ts index 0349352..e9f0ed7 100644 --- a/src/services/messagingService.ts +++ b/src/services/messagingService.ts @@ -4,39 +4,109 @@ * See LICENSE file in the project root for full license information */ +import { createDirectAuthMessagingService } from '../config/directMessaging.js'; +import { getSystemConfig } from '../config/getSystemConfig.js'; import getLogger from '../utils/logger.js'; +import { normalizePhoneNumber } from '../utils/utils.js'; const logger = getLogger('messaging'); -const isDevelopment = process.env.NODE_ENV === 'development'; +function shouldBypassDirectMessaging() { + const isDevelopment = process.env.NODE_ENV === 'development'; + const enableInDev = process.env.MESSAGING_ENABLE_IN_DEV === 'true'; + + return isDevelopment && !enableInDev; +} + +async function getMessagingService() { + const { app_name } = await getSystemConfig(); + return createDirectAuthMessagingService(app_name); +} export const sendOTPEmail = async (to: string, token: string) => { logger.debug(`Sending verification email to: ${to} with ${token}`); - if (isDevelopment) { + if (shouldBypassDirectMessaging()) { + logger.debug('Skipping direct email delivery in development'); return; } + + try { + const messaging = await getMessagingService(); + + await messaging.sendOtpEmail({ + to, + token, + }); + + logger.info('Verification email sent'); + } catch (error) { + logger.error(`Failed to send verification email ${error}`); + } }; export const sendOTPSMS = async (to: string, token: number) => { logger.debug(`Sending verification SMS: ${to} with ${token}`); - if (isDevelopment) { + + if (shouldBypassDirectMessaging()) { + logger.debug('Skipping direct SMS delivery in development'); return; } + + try { + const messaging = await getMessagingService(); + const normalizedPhone = normalizePhoneNumber(to); + + if (!normalizedPhone) { + throw new Error(`Invalid phone number for direct SMS delivery: ${to}`); + } + + await messaging.sendOtpSms({ + to: normalizedPhone, + token, + }); + } catch (error) { + logger.error(`Failed to send verification SMS ${error}`); + } }; export const sendMagicLinkEmail = async (to: string, token: string, safeRedirect: string) => { logger.debug(`Sending magic link to: ${to}. URL: ${safeRedirect}`); - if (isDevelopment) { + if (shouldBypassDirectMessaging()) { + logger.debug('Skipping direct magic link delivery in development'); return; } + + try { + const messaging = await getMessagingService(); + + await messaging.sendMagicLinkEmail({ + to, + token, + magicLinkUrl: safeRedirect, + }); + } catch (error) { + logger.error(`Failed to send magic link email ${error}`); + } }; export const sendBootstrapEmail = async (to: string, url: string) => { logger.debug(`Sending bootsrap invitation email to: ${to}. URL: ${url}`); - if (isDevelopment) { + if (shouldBypassDirectMessaging()) { + logger.debug('Skipping direct bootstrap delivery in development'); return; } + + try { + const messaging = await getMessagingService(); + + await messaging.sendBootstrapInviteEmail({ + to, + inviteUrl: url, + }); + } catch (error) { + logger.error(`Failed to send bootstrap invite email ${error}`); + } }; diff --git a/src/services/sessionIssuance.ts b/src/services/sessionIssuance.ts index d1db267..b78e697 100644 --- a/src/services/sessionIssuance.ts +++ b/src/services/sessionIssuance.ts @@ -9,7 +9,12 @@ import { Request, Response } from 'express'; import { getSystemConfig } from '../config/getSystemConfig.js'; import { clearBootstrapCookie } from '../lib/bootstrapCookie.js'; import { clearAuthCookies, setAuthCookies } from '../lib/cookie.js'; -import { generateRefreshToken, hashRefreshToken, signAccessToken } from '../lib/token.js'; +import { + createRefreshTokenLookup, + generateRefreshToken, + hashRefreshToken, + signAccessToken, +} from '../lib/token.js'; import { Session } from '../models/sessions.js'; import { computeSessionTimes, parseDurationToSeconds } from '../utils/utils.js'; @@ -32,6 +37,7 @@ export async function issueSessionAndRespond(params: IssueSessionParams): Promis const refreshToken = generateRefreshToken(); const refreshTokenHash = await hashRefreshToken(refreshToken); + const refreshTokenLookup = createRefreshTokenLookup(refreshToken); const { expiresAt, idleExpiresAt } = computeSessionTimes(); const session = await Session.create({ @@ -39,6 +45,7 @@ export async function issueSessionAndRespond(params: IssueSessionParams): Promis infraId: process.env.APP_ID!, mode: authMode, refreshTokenHash, + refreshTokenLookup, userAgent: req.get('user-agent'), ipAddress: req.ip, expiresAt, diff --git a/src/services/sessionService.ts b/src/services/sessionService.ts index 773fd90..017579c 100644 --- a/src/services/sessionService.ts +++ b/src/services/sessionService.ts @@ -4,9 +4,12 @@ * See LICENSE file in the project root for full license information */ +import { compareSync } from 'bcrypt-ts'; import { importSPKI, jwtVerify } from 'jose'; import jwt from 'jsonwebtoken'; +import { Op } from 'sequelize'; +import { createRefreshTokenLookup } from '../lib/token.js'; import { Session } from '../models/sessions.js'; import { User } from '../models/users.js'; import getLogger from '../utils/logger.js'; @@ -119,6 +122,62 @@ export async function validateAccessToken(token: string) { }; } +export interface RefreshSessionLookupResult { + session: Session | null; + legacyFallbackCandidates: number; + usedLegacyFallback: boolean; +} + +export async function findRefreshSessionByToken( + refreshToken: string, + now = new Date(), +): Promise { + const activeWhere = { + revokedAt: null, + expiresAt: { [Op.gt]: now }, + idleExpiresAt: { [Op.gt]: now }, + }; + + const refreshTokenLookup = createRefreshTokenLookup(refreshToken); + const session = await Session.findOne({ + where: { + ...activeWhere, + refreshTokenLookup, + }, + }); + + if (session) { + return { + session, + legacyFallbackCandidates: 0, + usedLegacyFallback: false, + }; + } + + const legacySessions = await Session.findAll({ + where: { + ...activeWhere, + refreshTokenLookup: null, + }, + }); + + for (const legacySession of legacySessions) { + if (compareSync(refreshToken, legacySession.refreshTokenHash)) { + return { + session: legacySession, + legacyFallbackCandidates: legacySessions.length, + usedLegacyFallback: true, + }; + } + } + + return { + session: null, + legacyFallbackCandidates: legacySessions.length, + usedLegacyFallback: legacySessions.length > 0, + }; +} + export async function validateSessionRecord(sessionId: string) { const session = await Session.findByPk(sessionId); if (!session) return null; @@ -152,7 +211,7 @@ export async function validateBearerToken(token: string) { try { payload = jwt.verify(token, serviceSecret, { - issuer: process.env.APP_ORIGIN, + issuer: process.env.APP_ORIGINS!.split(',')[0], audience: process.env.ISSUER, }); } catch (err: Error | unknown) { diff --git a/src/utils/otp.ts b/src/utils/otp.ts index c0dbe77..359bb8b 100644 --- a/src/utils/otp.ts +++ b/src/utils/otp.ts @@ -10,6 +10,10 @@ import getLogger from './logger.js'; const logger = getLogger('utils.otp'); +export interface GenerateOtpOptions { + sendMessage?: boolean; +} + export const generateRandomEmailOTP = (): string => { const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; let result = ''; @@ -23,7 +27,10 @@ export const generateRandomPhoneOTP = (): number => { return Math.floor(Math.random() * 900000) + 100000; }; -export const generateEmailOTP = async (user: User) => { +export const generateEmailOTP = async ( + user: User, + options: GenerateOtpOptions = {}, +): Promise => { if (!user) { throw new Error('Cannot generate email OTP for non-exsistent user'); } @@ -41,14 +48,21 @@ export const generateEmailOTP = async (user: User) => { emailVerificationTokenExpiry, }); - sendOTPEmail(user.email, emailToken); + if (options.sendMessage !== false) { + await sendOTPEmail(user.email, emailToken); + } + + return emailToken; } catch (error) { logger.error(`Error generate email OTP: ${error}`); throw new Error('Failed to set user OTP'); } }; -export const generatePhoneOTP = async (user: User) => { +export const generatePhoneOTP = async ( + user: User, + options: GenerateOtpOptions = {}, +): Promise => { if (!user) { throw new Error('Cannot generate phone OTP for non-exsistent user'); } @@ -66,7 +80,11 @@ export const generatePhoneOTP = async (user: User) => { phoneVerificationTokenExpiry, }); - sendOTPSMS(user.phone, phoneToken); + if (options.sendMessage !== false) { + await sendOTPSMS(user.phone, phoneToken); + } + + return phoneToken; } catch (error) { logger.error(`Error generate phone OTP: ${error}`); throw new Error('Failed to set user OTP'); diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 82011b3..0e6531f 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -20,6 +20,16 @@ export const isValidPhoneNumber = (phone: string): boolean => { return phoneNumber?.isValid() || false; }; +export const normalizePhoneNumber = (phone: string): string | null => { + const phoneNumber = parsePhoneNumberFromString(phone); + + if (!phoneNumber?.isValid()) { + return null; + } + + return phoneNumber.number; +}; + export function computeSessionTimes(now = new Date()) { const expiresAt = new Date(now.getTime() + MAX_SESSION_LIFETIME_DAYS * 24 * 60 * 60 * 1000); const idleExpiresAt = new Date(now.getTime() + IDLE_TIMEOUT_DAYS * 24 * 60 * 60 * 1000); diff --git a/tests/e2e/auth.happy.spec.ts b/tests/e2e/auth.happy.spec.ts index ac0b5e4..f43a01a 100644 --- a/tests/e2e/auth.happy.spec.ts +++ b/tests/e2e/auth.happy.spec.ts @@ -26,8 +26,13 @@ vi.unmock('../../src/utils/secretStore.js'); vi.unmock('bcrypt-ts'); let app: any; +const shouldRunE2E = process.env.CI !== 'true' && process.env.TEST_DB === 'postgres'; beforeAll(async () => { + if (!shouldRunE2E) { + return; + } + vi.stubEnv('NODE_ENV', 'test'); vi.stubEnv('AUTH_MODE', 'web'); @@ -40,7 +45,6 @@ beforeAll(async () => { vi.stubEnv('ISSUER', 'test-issuer'); vi.stubEnv('APP_ID', 'test-app'); - vi.stubEnv('APP_ORIGIN', 'http://localhost'); vi.stubEnv('JWKS_ACTIVE_KIDe', 'dev-main'); vi.stubEnv('API_SERVICE_TOKEN', 'service-token'); @@ -71,9 +75,7 @@ afterAll(() => { vi.unstubAllEnvs(); }); -const isCI = process.env.CI === 'true'; - -(isCI ? it.skip : it)('full auth lifecycle works', async () => { +(shouldRunE2E ? it : it.skip)('full auth lifecycle works', async () => { const email = 'test@example.com'; const phone = '+14155552671'; diff --git a/tests/e2e/authFlow.spec.ts b/tests/e2e/authFlow.spec.ts index e3d2a8b..cd55f1a 100644 --- a/tests/e2e/authFlow.spec.ts +++ b/tests/e2e/authFlow.spec.ts @@ -7,10 +7,6 @@ vi.mock('../../src/config/getSystemConfig.js', () => ({ getSystemConfig: vi.fn(), })); -vi.mock('bcrypt-ts', () => ({ - compareSync: vi.fn(), -})); - vi.mock('../../../src/services/authEventService.js', () => ({ AuthEventService: { log: vi.fn(), @@ -30,11 +26,13 @@ import { import { generatePhoneOTP, verifyPhoneOTP } from '../../src/utils/otp.js'; -import { validateAccessToken } from '../../src/services/sessionService.js'; +import { + findRefreshSessionByToken, + validateAccessToken, +} from '../../src/services/sessionService.js'; import { getSystemConfig } from '../../src/config/getSystemConfig.js'; -import { compareSync } from 'bcrypt-ts'; import { buildRegistrationRequest } from '../factories/requestFactory.js'; import { buildUser } from '../factories/userFactory.js'; import { buildSession } from '../factories/sessionFactory.js'; @@ -57,8 +55,6 @@ beforeEach(() => { (generateRefreshToken as any).mockReturnValue('refresh-token'); (hashRefreshToken as any).mockResolvedValue('hashed-refresh'); (Session.create as any).mockResolvedValue({ id: 'session-1' }); - - (compareSync as any).mockReturnValue(true); }); describe('E2E Auth Flow', () => { @@ -114,6 +110,11 @@ describe('E2E Auth Flow', () => { expect(accessRes.status).toBe(200); (validateAccessToken as any).mockResolvedValue(null); + (findRefreshSessionByToken as any).mockResolvedValue({ + session: buildSession(), + legacyFallbackCandidates: 0, + usedLegacyFallback: false, + }); (User.findByPk as any).mockResolvedValue({ id: 'user-1', diff --git a/tests/integration/auth/cookieAuth.security.spec.ts b/tests/integration/auth/cookieAuth.security.spec.ts index 6a21bf6..5acfb7d 100644 --- a/tests/integration/auth/cookieAuth.security.spec.ts +++ b/tests/integration/auth/cookieAuth.security.spec.ts @@ -1,5 +1,4 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { compareSync } from 'bcrypt-ts'; import { verifyCookieAuth } from '../../../src/middleware/verifyCookieAuth.js'; import { clearAuthCookies, setAuthCookies } from '../../../src/lib/cookie.js'; @@ -7,6 +6,7 @@ import { Session } from '../../../src/models/sessions.js'; import { User } from '../../../src/models/users.js'; import { AuthEventService } from '../../../src/services/authEventService.js'; import { + findRefreshSessionByToken, getUserFromSession, hardRevokeSession, revokeSessionChain, @@ -14,7 +14,12 @@ import { validateSessionRecord, verifyJwtWithKid, } from '../../../src/services/sessionService.js'; -import { generateRefreshToken, hashRefreshToken, signAccessToken } from '../../../src/lib/token.js'; +import { + createRefreshTokenLookup, + generateRefreshToken, + hashRefreshToken, + signAccessToken, +} from '../../../src/lib/token.js'; function mockReqRes(cookies: Record = {}) { const req: any = { @@ -44,6 +49,7 @@ function buildRefreshSession(overrides: Record = {}) { infraId: 'app-1', mode: 'web', refreshTokenHash: 'hashed-refresh', + refreshTokenLookup: 'refresh-lookup', userAgent: 'vitest', ipAddress: '127.0.0.1', replacedBySessionId: null, @@ -56,10 +62,9 @@ function buildRefreshSession(overrides: Record = {}) { beforeEach(() => { vi.clearAllMocks(); - (compareSync as any).mockReturnValue(false); - (generateRefreshToken as any).mockReturnValue('new-refresh-token'); (hashRefreshToken as any).mockResolvedValue('new-refresh-hash'); + (createRefreshTokenLookup as any).mockReturnValue('new-refresh-lookup'); (signAccessToken as any).mockResolvedValue('new-access-token'); (Session.create as any).mockResolvedValue({ id: 'session-2' }); @@ -88,6 +93,9 @@ describe('verifyCookieAuth security - ephemeral', () => { await middleware(req, res, next); + expect(clearAuthCookies).toHaveBeenCalledWith(res); + expect(res.status).toHaveBeenCalledWith(401); + expect(res.json).toHaveBeenCalledWith({ error: 'unauthorized' }); expect(next).not.toHaveBeenCalled(); }); }); @@ -128,7 +136,11 @@ describe('verifyCookieAuth security - access path', () => { describe('verifyCookieAuth security - silent refresh', () => { it('returns 401 when refresh cookie is present but no matching session is found', async () => { (validateAccessToken as any).mockResolvedValue(null); - (Session.findAll as any).mockResolvedValue([]); + (findRefreshSessionByToken as any).mockResolvedValue({ + session: null, + legacyFallbackCandidates: 0, + usedLegacyFallback: false, + }); (AuthEventService.serviceTokenInvalid as any).mockResolvedValue(undefined); const middleware = verifyCookieAuth('access'); @@ -145,13 +157,16 @@ describe('verifyCookieAuth security - silent refresh', () => { it('detects refresh token reuse when session was already replaced', async () => { (validateAccessToken as any).mockResolvedValue(null); - (compareSync as any).mockReturnValue(true); const reusedSession = buildRefreshSession({ replacedBySessionId: 'session-2', }); - (Session.findAll as any).mockResolvedValue([reusedSession]); + (findRefreshSessionByToken as any).mockResolvedValue({ + session: reusedSession, + legacyFallbackCandidates: 0, + usedLegacyFallback: false, + }); (revokeSessionChain as any).mockResolvedValue(undefined); (AuthEventService.serviceTokenInvalid as any).mockResolvedValue(undefined); @@ -170,13 +185,16 @@ describe('verifyCookieAuth security - silent refresh', () => { it('detects refresh token reuse when session is already revoked', async () => { (validateAccessToken as any).mockResolvedValue(null); - (compareSync as any).mockReturnValue(true); const revokedSession = buildRefreshSession({ revokedAt: new Date(), }); - (Session.findAll as any).mockResolvedValue([revokedSession]); + (findRefreshSessionByToken as any).mockResolvedValue({ + session: revokedSession, + legacyFallbackCandidates: 0, + usedLegacyFallback: false, + }); (revokeSessionChain as any).mockResolvedValue(undefined); (AuthEventService.serviceTokenInvalid as any).mockResolvedValue(undefined); @@ -194,11 +212,14 @@ describe('verifyCookieAuth security - silent refresh', () => { it('hard-revokes when refresh session user no longer exists', async () => { (validateAccessToken as any).mockResolvedValue(null); - (compareSync as any).mockReturnValue(true); const session = buildRefreshSession(); - (Session.findAll as any).mockResolvedValue([session]); + (findRefreshSessionByToken as any).mockResolvedValue({ + session, + legacyFallbackCandidates: 0, + usedLegacyFallback: false, + }); (User.findByPk as any).mockResolvedValue(null); (hardRevokeSession as any).mockResolvedValue(undefined); @@ -216,11 +237,14 @@ describe('verifyCookieAuth security - silent refresh', () => { it('rotates session and sets fresh cookies on successful refresh', async () => { (validateAccessToken as any).mockResolvedValue(null); - (compareSync as any).mockReturnValue(true); const session = buildRefreshSession(); - (Session.findAll as any).mockResolvedValue([session]); + (findRefreshSessionByToken as any).mockResolvedValue({ + session, + legacyFallbackCandidates: 0, + usedLegacyFallback: false, + }); (User.findByPk as any).mockResolvedValue({ id: 'user-1' }); const middleware = verifyCookieAuth('access'); @@ -231,6 +255,9 @@ describe('verifyCookieAuth security - silent refresh', () => { await middleware(req, res, next); expect(Session.create).toHaveBeenCalled(); + expect(Session.create).toHaveBeenCalledWith( + expect.objectContaining({ refreshTokenLookup: 'new-refresh-lookup' }), + ); expect(session.save).toHaveBeenCalled(); expect(setAuthCookies).toHaveBeenCalledWith( res, diff --git a/tests/integration/auth/cookieAuth.spec.ts b/tests/integration/auth/cookieAuth.spec.ts index 2b3fbd3..4908004 100644 --- a/tests/integration/auth/cookieAuth.spec.ts +++ b/tests/integration/auth/cookieAuth.spec.ts @@ -1,7 +1,9 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { verifyCookieAuth } from '../../../src/middleware/verifyCookieAuth.js'; +import { clearAuthCookies } from '../../../src/lib/cookie.js'; import { + findRefreshSessionByToken, validateAccessToken, validateSessionRecord, getUserFromSession, @@ -14,14 +16,9 @@ vi.mock('../../../src/models/authEvents.js', () => ({ }, })); -vi.mock('bcrypt-ts', () => ({ - compareSync: vi.fn(), -})); - import { User } from '../../../src/models/users.js'; import { Session } from '../../../src/models/sessions.js'; import { generateRefreshToken, hashRefreshToken, signAccessToken } from '../../../src/lib/token.js'; -import { compareSync } from 'bcrypt-ts'; function mockReqRes(cookies: any = {}) { const req: any = { @@ -73,6 +70,23 @@ describe('verifyCookieAuth - ephemeral', () => { expect(req.user).toBeDefined(); expect(next).toHaveBeenCalled(); }); + + it('rejects invalid ephemeral token with 401', async () => { + (verifyJwtWithKid as any).mockResolvedValue(null); + + const middleware = verifyCookieAuth('ephemeral'); + + const { req, res, next } = mockReqRes({ + seamless_ephemeral: 'bad-token', + }); + + await middleware(req, res, next); + + expect(clearAuthCookies).toHaveBeenCalledWith(res); + expect(res.status).toHaveBeenCalledWith(401); + expect(res.json).toHaveBeenCalledWith({ error: 'unauthorized' }); + expect(next).not.toHaveBeenCalled(); + }); }); describe('verifyCookieAuth - access token', () => { @@ -115,11 +129,8 @@ describe('verifyCookieAuth - access token', () => { describe('verifyCookieAuth - silent refresh', () => { it('refreshes session when access token invalid', async () => { (validateAccessToken as any).mockResolvedValue(null); - - (compareSync as any).mockReturnValue(true); - - (Session.findAll as any).mockResolvedValue([ - { + (findRefreshSessionByToken as any).mockResolvedValue({ + session: { id: 'session-1', refreshTokenHash: 'hash', userId: 'user-1', @@ -130,7 +141,9 @@ describe('verifyCookieAuth - silent refresh', () => { revokedAt: null, save: vi.fn(), }, - ]); + legacyFallbackCandidates: 0, + usedLegacyFallback: false, + }); (User.findByPk as any).mockResolvedValue({ id: 'user-1', diff --git a/tests/integration/authentication/authentication.spec.ts b/tests/integration/authentication/authentication.spec.ts index b61d835..f030a9a 100644 --- a/tests/integration/authentication/authentication.spec.ts +++ b/tests/integration/authentication/authentication.spec.ts @@ -8,25 +8,32 @@ import { Session } from '../../../src/models/sessions'; import { User } from '../../../src/models/users'; import { buildUser } from '../../factories/userFactory'; import { + createRefreshTokenLookup, generateRefreshToken, hashRefreshToken, signAccessToken, signEphemeralToken, } from '../../../src/lib/token'; -import { compareSync } from 'bcrypt-ts'; -import { getSecret } from '../../../src/utils/secretsStore'; +import { findRefreshSessionByToken } from '../../../src/services/sessionService'; let app: Application; -vi.mock('../../../src/middleware/attachAuthMiddleware.js', () => ({ - attachAuthMiddleware: () => (req: any, _res: any, next: any) => { - req.user = buildUser(); - req.sessionId = 'session-1'; - next(); - }, -})); +vi.mock('../../../src/middleware/attachAuthMiddleware.js', async (importOriginal) => { + const actual = + await importOriginal(); + + return { + ...actual, + attachAuthMiddleware: () => (req: any, _res: any, next: any) => { + req.user = buildUser(); + req.sessionId = 'session-1'; + next(); + }, + }; +}); beforeAll(async () => { + vi.stubEnv('AUTH_MODE', 'server'); app = await createApp(); }); @@ -113,30 +120,18 @@ describe('POST /refresh', () => { }); it('rejects invalid session', async () => { - const jwt = await import('jsonwebtoken'); - - (jwt.default.verify as any).mockReturnValue({ - refreshToken: 'token', + (findRefreshSessionByToken as any).mockResolvedValue({ + session: null, + legacyFallbackCandidates: 0, + usedLegacyFallback: false, }); - (getSecret as any).mockResolvedValue('secret'); - - (Session.findAll as any).mockResolvedValue([]); - - const res = await request(app).post('/refresh').set('Authorization', 'Bearer token'); + const res = await request(app).post('/refresh').set('Authorization', 'Bearer refresh-token'); expect(res.status).toBe(401); }); it('refreshes session successfully', async () => { - const jwt = await import('jsonwebtoken'); - - (jwt.default.verify as any).mockReturnValue({ - refreshToken: 'token', - }); - - (getSecret as any).mockResolvedValue('secret'); - const session = { id: 'session-1', refreshTokenHash: 'hash', @@ -149,9 +144,11 @@ describe('POST /refresh', () => { save: vi.fn(), }; - (Session.findAll as any).mockResolvedValue([session]); - - (compareSync as any).mockReturnValue(true); + (findRefreshSessionByToken as any).mockResolvedValue({ + session, + legacyFallbackCandidates: 0, + usedLegacyFallback: false, + }); (User.findByPk as any).mockResolvedValue(buildUser()); @@ -160,14 +157,24 @@ describe('POST /refresh', () => { (signAccessToken as any).mockResolvedValue('access'); (generateRefreshToken as any).mockReturnValue('refresh'); (hashRefreshToken as any).mockResolvedValue('hash'); + (createRefreshTokenLookup as any).mockReturnValue('refresh-lookup'); (getSystemConfig as any).mockResolvedValue({ access_token_ttl: '15m', refresh_token_ttl: '1h', }); - const res = await request(app).post('/refresh').set('Authorization', 'Bearer token'); + const res = await request(app).post('/refresh').set('Authorization', 'Bearer refresh-token'); expect(res.status).toBe(200); + expect(signAccessToken).toHaveBeenCalledWith( + 'new-session', + expect.any(String), + expect.any(Array), + ); + expect(Session.create).toHaveBeenCalledWith( + expect.objectContaining({ refreshTokenLookup: 'refresh-lookup' }), + ); + expect(res.body.refreshToken).toBe('refresh'); }); }); diff --git a/tests/integration/internal/internal.spec.ts b/tests/integration/internal/internal.spec.ts index a0faa10..cc41fc1 100644 --- a/tests/integration/internal/internal.spec.ts +++ b/tests/integration/internal/internal.spec.ts @@ -9,13 +9,19 @@ import { AuthEvent } from '../../../src/models/authEvents'; let app: Application; -vi.mock('../../../src/middleware/attachAuthMiddleware.js', () => ({ - attachAuthMiddleware: () => (req: any, _res: any, next: any) => { - req.user = buildUser(); - req.sessionId = 'session-1'; - next(); - }, -})); +vi.mock('../../../src/middleware/attachAuthMiddleware.js', async (importOriginal) => { + const actual = + await importOriginal(); + + return { + ...actual, + attachAuthMiddleware: () => (req: any, _res: any, next: any) => { + req.user = buildUser(); + req.sessionId = 'session-1'; + next(); + }, + }; +}); beforeAll(async () => { app = await createApp(); diff --git a/tests/integration/registration/register.spec.ts b/tests/integration/registration/register.spec.ts index 3d9d1af..0c8aac3 100644 --- a/tests/integration/registration/register.spec.ts +++ b/tests/integration/registration/register.spec.ts @@ -4,6 +4,7 @@ import { createApp } from '../../../src/app'; import { Application } from 'express'; import { buildUser } from '../../factories/userFactory.js'; +import { generatePhoneOTP } from '../../../src/utils/otp.js'; vi.mock('../../../src/models/users.js', () => ({ User: { @@ -46,6 +47,7 @@ beforeEach(() => { }); (signEphemeralToken as any).mockResolvedValue('mock-token'); + (generatePhoneOTP as any).mockResolvedValue(123456); }); describe('POST /registration/register', () => { @@ -78,6 +80,52 @@ describe('POST /registration/register', () => { expect(signEphemeralToken).toHaveBeenCalledWith(user.id); }); + it('rejects when email belongs to one user and phone is new', async () => { + (User.findOne as any) + .mockResolvedValueOnce(buildUser({ email: 'test@example.com', phone: '+14155552671' })) + .mockResolvedValueOnce(null); + + const res = await request(app) + .post('/registration/register') + .send(buildRegistrationRequest({ phone: '+14155550000' })); + + expect(res.status).toBe(409); + expect(res.body.error).toBe('Registration conflict'); + expect(User.create).not.toHaveBeenCalled(); + expect(signEphemeralToken).not.toHaveBeenCalled(); + expect(generatePhoneOTP).not.toHaveBeenCalled(); + }); + + it('rejects when phone belongs to one user and email is new', async () => { + (User.findOne as any) + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(buildUser({ email: 'test@example.com', phone: '+14155552671' })); + + const res = await request(app) + .post('/registration/register') + .send(buildRegistrationRequest({ email: 'other@example.com' })); + + expect(res.status).toBe(409); + expect(res.body.error).toBe('Registration conflict'); + expect(User.create).not.toHaveBeenCalled(); + expect(signEphemeralToken).not.toHaveBeenCalled(); + expect(generatePhoneOTP).not.toHaveBeenCalled(); + }); + + it('rejects when email and phone belong to different existing users', async () => { + (User.findOne as any) + .mockResolvedValueOnce(buildUser({ id: 'user-1', email: 'test@example.com' })) + .mockResolvedValueOnce(buildUser({ id: 'user-2', phone: '+14155552671' })); + + const res = await request(app).post('/registration/register').send(buildRegistrationRequest()); + + expect(res.status).toBe(409); + expect(res.body.error).toBe('Registration conflict'); + expect(User.create).not.toHaveBeenCalled(); + expect(signEphemeralToken).not.toHaveBeenCalled(); + expect(generatePhoneOTP).not.toHaveBeenCalled(); + }); + it('fails without email', async () => { const res = await request(app).post('/registration/register').send({ phone: '+15555555555' }); diff --git a/tests/integration/session/session.security.spec.ts b/tests/integration/session/session.security.spec.ts index efd5da5..c38698f 100644 --- a/tests/integration/session/session.security.spec.ts +++ b/tests/integration/session/session.security.spec.ts @@ -14,13 +14,19 @@ let mockUser: any = { roles: ['user'], }; -vi.mock('../../../src/middleware/attachAuthMiddleware.js', () => ({ - attachAuthMiddleware: () => (req: any, _res: any, next: any) => { - req.user = mockUser; - req.sessionId = 'session-1'; - next(); - }, -})); +vi.mock('../../../src/middleware/attachAuthMiddleware.js', async (importOriginal) => { + const actual = + await importOriginal(); + + return { + ...actual, + attachAuthMiddleware: () => (req: any, _res: any, next: any) => { + req.user = mockUser; + req.sessionId = 'session-1'; + next(); + }, + }; +}); let app: Application; diff --git a/tests/integration/webauthn/webauthn.spec.ts b/tests/integration/webauthn/webauthn.spec.ts index c2327a1..7254d2e 100644 --- a/tests/integration/webauthn/webauthn.spec.ts +++ b/tests/integration/webauthn/webauthn.spec.ts @@ -14,13 +14,19 @@ import { generateRefreshToken, hashRefreshToken, signAccessToken } from '../../. let app: Application; -vi.mock('../../../src/middleware/attachAuthMiddleware.js', () => ({ - attachAuthMiddleware: () => (req: any, _res: any, next: any) => { - req.user = buildUser(); - req.sessionId = 'session-1'; - next(); - }, -})); +vi.mock('../../../src/middleware/attachAuthMiddleware.js', async (importOriginal) => { + const actual = + await importOriginal(); + + return { + ...actual, + attachAuthMiddleware: () => (req: any, _res: any, next: any) => { + req.user = buildUser(); + req.sessionId = 'session-1'; + next(); + }, + }; +}); beforeAll(async () => { app = await createApp(); diff --git a/tests/setup/env.ts b/tests/setup/env.ts index d01ba5a..784e424 100644 --- a/tests/setup/env.ts +++ b/tests/setup/env.ts @@ -1,6 +1,6 @@ process.env.NODE_ENV = 'test'; process.env.AUTH_MODE = 'api'; -process.env.APP_ORIGIN = 'http://localhost:5174'; +process.env.APP_ORIGINS = 'http://localhost:5174'; // Default: use mock DB mode process.env.TEST_DB = process.env.TEST_DB || 'mock'; diff --git a/tests/setup/mocks.ts b/tests/setup/mocks.ts index f0838d7..efac91d 100644 --- a/tests/setup/mocks.ts +++ b/tests/setup/mocks.ts @@ -65,39 +65,46 @@ vi.mock('../../src/config/getSystemConfig.js', () => ({ vi.mock('../../src/services/sessionService.js', () => ({ validateAccessToken: vi.fn(), validateSessionRecord: vi.fn(), + findRefreshSessionByToken: vi.fn(), getUserFromSession: vi.fn(), verifyJwtWithKid: vi.fn(), revokeSessionChain: vi.fn(), hardRevokeSession: vi.fn(), })); -vi.mock('../../src/middleware/attachAuthMiddleware.js', () => ({ - attachAuthMiddleware: () => (req: any, _res: any, next: any) => { - // inject fake authenticated user - req.user = { - id: 'user-1', - email: 'test@example.com', - phone: '+14155552671', - roles: ['user'], +vi.mock('../../src/middleware/attachAuthMiddleware.js', async (importOriginal) => { + const actual = + await importOriginal(); - // required for verification flows - emailVerificationToken: '123456', - emailVerificationTokenExpiry: new Date(Date.now() + 100000), - - phoneVerificationToken: '123456', - phoneVerificationTokenExpiry: new Date(Date.now() + 100000), - - verified: true, - emailVerified: true, - phoneVerified: true, - - update: vi.fn(), - }; - - req.sessionId = 'session-1'; - next(); - }, -})); + return { + ...actual, + attachAuthMiddleware: () => (req: any, _res: any, next: any) => { + // inject fake authenticated user + req.user = { + id: 'user-1', + email: 'test@example.com', + phone: '+14155552671', + roles: ['user'], + + // required for verification flows + emailVerificationToken: '123456', + emailVerificationTokenExpiry: new Date(Date.now() + 100000), + + phoneVerificationToken: '123456', + phoneVerificationTokenExpiry: new Date(Date.now() + 100000), + + verified: true, + emailVerified: true, + phoneVerified: true, + + update: vi.fn(), + }; + + req.sessionId = 'session-1'; + next(); + }, + }; +}); vi.mock('../../src/middleware/authenticateServiceToken.js', () => ({ verifyServiceToken: (_req: any, _res: any, next: any) => { @@ -130,6 +137,7 @@ vi.mock('../../src/lib/token.js', () => ({ signAccessToken: vi.fn(), generateRefreshToken: vi.fn(), hashRefreshToken: vi.fn(), + createRefreshTokenLookup: vi.fn(), })); vi.mock('../../src/lib/cookie.js', () => ({ diff --git a/tests/unit/config/directMessaging.spec.ts b/tests/unit/config/directMessaging.spec.ts new file mode 100644 index 0000000..f648748 --- /dev/null +++ b/tests/unit/config/directMessaging.spec.ts @@ -0,0 +1,115 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const createAuthMessagingServiceMock = vi.fn(); +const createAwsEmailTransportMock = vi.fn(); +const createAwsSmsTransportMock = vi.fn(); +const createTwilioSmsTransportMock = vi.fn(); + +vi.unmock('../../../src/config/directMessaging.js'); + +vi.mock('@seamless-auth/messaging', () => { + class MessagingConfigurationError extends Error {} + + return { + MessagingConfigurationError, + createAuthMessagingService: createAuthMessagingServiceMock, + }; +}); + +vi.mock('@seamless-auth/messaging-aws', () => ({ + createAwsEmailTransport: createAwsEmailTransportMock, + createAwsSmsTransport: createAwsSmsTransportMock, +})); + +vi.mock('@seamless-auth/messaging-twilio', () => ({ + createTwilioSmsTransport: createTwilioSmsTransportMock, +})); + +describe('directMessaging config', () => { + beforeEach(() => { + vi.resetModules(); + vi.clearAllMocks(); + + delete process.env.MESSAGING_AWS_REGION; + delete process.env.AWS_REGION; + delete process.env.REGION; + delete process.env.MESSAGING_EMAIL_FROM; + delete process.env.SES_EMAIL; + delete process.env.MESSAGING_SMS_PROVIDER; + delete process.env.SMS_PROVIDER; + delete process.env.MESSAGING_TWILIO_ACCOUNT_SID; + delete process.env.TWILIO_ACCOUNT_SID; + delete process.env.MESSAGING_TWILIO_AUTH_TOKEN; + delete process.env.TWILIO_AUTH_TOKEN; + delete process.env.MESSAGING_SMS_FROM; + delete process.env.TWILIO_PHONE_NUMBER; + + createAwsEmailTransportMock.mockReturnValue({ name: 'aws-email' }); + createAwsSmsTransportMock.mockReturnValue({ name: 'aws-sms' }); + createTwilioSmsTransportMock.mockReturnValue({ name: 'twilio-sms' }); + createAuthMessagingServiceMock.mockReturnValue({ name: 'messaging-service' }); + }); + + it('builds direct messaging with AWS defaults', async () => { + process.env.AWS_REGION = 'us-east-1'; + process.env.SES_EMAIL = 'noreply@example.com'; + + const { createDirectAuthMessagingService } = + await import('../../../src/config/directMessaging.js'); + + const result = createDirectAuthMessagingService('Test App'); + + expect(createAwsEmailTransportMock).toHaveBeenCalledWith({ + region: 'us-east-1', + fromEmail: 'noreply@example.com', + }); + expect(createAwsSmsTransportMock).toHaveBeenCalledWith({ + region: 'us-east-1', + senderId: undefined, + }); + expect(createAuthMessagingServiceMock).toHaveBeenCalledWith({ + appName: 'Test App', + email: { name: 'aws-email' }, + sms: { name: 'aws-sms' }, + }); + expect(result).toEqual({ name: 'messaging-service' }); + }); + + it('builds Twilio SMS transport when configured', async () => { + process.env.MESSAGING_AWS_REGION = 'us-west-2'; + process.env.MESSAGING_EMAIL_FROM = 'alerts@example.com'; + process.env.MESSAGING_SMS_PROVIDER = 'twilio'; + process.env.MESSAGING_TWILIO_ACCOUNT_SID = 'sid'; + process.env.MESSAGING_TWILIO_AUTH_TOKEN = 'token'; + process.env.MESSAGING_SMS_FROM = '+15555550123'; + + const { createDirectAuthMessagingService } = + await import('../../../src/config/directMessaging.js'); + + createDirectAuthMessagingService('Test App'); + + expect(createAwsEmailTransportMock).toHaveBeenCalledWith({ + region: 'us-west-2', + fromEmail: 'alerts@example.com', + }); + expect(createTwilioSmsTransportMock).toHaveBeenCalledWith({ + accountSid: 'sid', + authToken: 'token', + fromNumber: '+15555550123', + }); + expect(createAwsSmsTransportMock).not.toHaveBeenCalled(); + }); + + it('throws for an unsupported SMS provider', async () => { + process.env.MESSAGING_AWS_REGION = 'us-east-1'; + process.env.MESSAGING_EMAIL_FROM = 'noreply@example.com'; + process.env.MESSAGING_SMS_PROVIDER = 'postal-pigeon'; + + const { createDirectAuthMessagingService } = + await import('../../../src/config/directMessaging.js'); + + expect(() => createDirectAuthMessagingService('Test App')).toThrow( + 'Unsupported MESSAGING_SMS_PROVIDER "postal-pigeon"', + ); + }); +}); diff --git a/tests/unit/controllers/authentication.spec.ts b/tests/unit/controllers/authentication.spec.ts new file mode 100644 index 0000000..39c5543 --- /dev/null +++ b/tests/unit/controllers/authentication.spec.ts @@ -0,0 +1,177 @@ +/* + * Copyright © 2026 Fells Code, LLC + * Licensed under the GNU Affero General Public License v3.0 + * See LICENSE file in the project root for full license information + */ + +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { buildUser } from '../../factories/userFactory.js'; + +function mockReqRes(authorization?: string) { + const req: any = { + ip: '127.0.0.1', + cookies: {}, + headers: { + 'user-agent': 'vitest', + ...(authorization ? { authorization } : {}), + }, + }; + + const res: any = { + status: vi.fn().mockReturnThis(), + json: vi.fn().mockReturnThis(), + }; + + return { req, res }; +} + +async function loadAuthenticationModule() { + vi.stubEnv('AUTH_MODE', 'server'); + + const [ + { refreshSession }, + { getSystemConfig }, + { Session }, + { User }, + { AuthEventService }, + tokenLib, + cookieLib, + sessionService, + ] = await Promise.all([ + import('../../../src/controllers/authentication.js'), + import('../../../src/config/getSystemConfig.js'), + import('../../../src/models/sessions.js'), + import('../../../src/models/users.js'), + import('../../../src/services/authEventService.js'), + import('../../../src/lib/token.js'), + import('../../../src/lib/cookie.js'), + import('../../../src/services/sessionService.js'), + ]); + + return { + refreshSession, + getSystemConfig, + Session, + User, + AuthEventService, + findRefreshSessionByToken: sessionService.findRefreshSessionByToken, + generateRefreshToken: tokenLib.generateRefreshToken, + hashRefreshToken: tokenLib.hashRefreshToken, + createRefreshTokenLookup: tokenLib.createRefreshTokenLookup, + signAccessToken: tokenLib.signAccessToken, + setAuthCookies: cookieLib.setAuthCookies, + }; +} + +beforeEach(() => { + vi.clearAllMocks(); +}); + +describe('refreshSession', () => { + it('rejects missing refresh token in server mode', async () => { + const { refreshSession, AuthEventService } = await loadAuthenticationModule(); + const { req, res } = mockReqRes(); + + await refreshSession(req, res); + + expect(AuthEventService.log).toHaveBeenCalledWith( + expect.objectContaining({ + userId: null, + type: 'refresh_token_failed', + metadata: { reason: 'Missing refresh token' }, + }), + ); + expect(res.status).toHaveBeenCalledWith(401); + expect(res.json).toHaveBeenCalledWith({ error: 'Not allowed' }); + }); + + it('rejects refresh tokens that do not resolve to a session', async () => { + const { refreshSession, AuthEventService, findRefreshSessionByToken } = + await loadAuthenticationModule(); + const { req, res } = mockReqRes('Bearer raw-refresh-token'); + + (findRefreshSessionByToken as any).mockResolvedValue({ + session: null, + legacyFallbackCandidates: 2, + usedLegacyFallback: true, + }); + (AuthEventService.serviceTokenInvalid as any).mockResolvedValue(undefined); + + await refreshSession(req, res); + + expect(findRefreshSessionByToken).toHaveBeenCalledWith('raw-refresh-token', expect.any(Date)); + expect(AuthEventService.serviceTokenInvalid).toHaveBeenCalledWith(req); + expect(res.status).toHaveBeenCalledWith(401); + expect(res.json).toHaveBeenCalledWith({ error: 'invalid_refresh_token' }); + }); + + it('rotates the session using the raw bearer refresh token in server mode', async () => { + const { + refreshSession, + getSystemConfig, + Session, + User, + findRefreshSessionByToken, + generateRefreshToken, + hashRefreshToken, + createRefreshTokenLookup, + signAccessToken, + setAuthCookies, + } = await loadAuthenticationModule(); + const { req, res } = mockReqRes('Bearer raw-refresh-token'); + const user = buildUser({ id: 'user-1', roles: ['admin'] }); + const session = { + id: 'session-1', + refreshTokenHash: 'stored-refresh-hash', + replacedBySessionId: null, + revokedAt: null, + userId: user.id, + infraId: 'app', + mode: 'server', + userAgent: 'vitest', + save: vi.fn(), + }; + + (findRefreshSessionByToken as any).mockResolvedValue({ + session, + legacyFallbackCandidates: 0, + usedLegacyFallback: false, + }); + (User.findByPk as any).mockResolvedValue(user); + (generateRefreshToken as any).mockReturnValue('new-raw-refresh-token'); + (hashRefreshToken as any).mockResolvedValue('new-refresh-hash'); + (createRefreshTokenLookup as any).mockReturnValue('new-refresh-lookup'); + (Session.create as any).mockResolvedValue({ id: 'session-2' }); + (signAccessToken as any).mockResolvedValue('new-access-token'); + (getSystemConfig as any).mockResolvedValue({ + access_token_ttl: '15m', + refresh_token_ttl: '1h', + }); + + await refreshSession(req, res); + + expect(findRefreshSessionByToken).toHaveBeenCalledWith('raw-refresh-token', expect.any(Date)); + expect(Session.create).toHaveBeenCalledWith( + expect.objectContaining({ + userId: user.id, + refreshTokenHash: 'new-refresh-hash', + refreshTokenLookup: 'new-refresh-lookup', + }), + ); + expect(signAccessToken).toHaveBeenCalledWith('session-2', user.id, user.roles); + expect(setAuthCookies).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith({ + message: 'Success', + token: 'new-access-token', + refreshToken: 'new-raw-refresh-token', + sub: user.id, + roles: user.roles, + email: user.email, + phone: user.phone, + ttl: 900, + refreshTtl: 3600, + }); + }); +}); diff --git a/tests/unit/controllers/otp.spec.ts b/tests/unit/controllers/otp.spec.ts new file mode 100644 index 0000000..2bf2cb9 --- /dev/null +++ b/tests/unit/controllers/otp.spec.ts @@ -0,0 +1,360 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const setAuthCookiesMock = vi.fn(); +const signEphemeralTokenMock = vi.fn(); +const authEventLogMock = vi.fn(); +const issueSessionAndRespondMock = vi.fn(); +const generateEmailOTPMock = vi.fn(); +const generatePhoneOTPMock = vi.fn(); +const verifyEmailOTPMock = vi.fn(); +const verifyPhoneOTPMock = vi.fn(); +const isValidEmailMock = vi.fn(); +const isValidPhoneNumberMock = vi.fn(); +const normalizePhoneNumberMock = vi.fn(); +const loggerMock = { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn(), +}; + +vi.mock('../../../src/lib/cookie.js', () => ({ + setAuthCookies: setAuthCookiesMock, +})); + +vi.mock('../../../src/lib/token.js', () => ({ + signEphemeralToken: signEphemeralTokenMock, +})); + +vi.mock('../../../src/services/authEventService.js', () => ({ + AuthEventService: { + log: authEventLogMock, + }, +})); + +vi.mock('../../../src/services/sessionIssuance.js', () => ({ + issueSessionAndRespond: issueSessionAndRespondMock, +})); + +vi.mock('../../../src/utils/otp.js', () => ({ + generateEmailOTP: generateEmailOTPMock, + generatePhoneOTP: generatePhoneOTPMock, + verifyEmailOTP: verifyEmailOTPMock, + verifyPhoneOTP: verifyPhoneOTPMock, +})); + +vi.mock('../../../src/utils/utils.js', () => ({ + isValidEmail: isValidEmailMock, + isValidPhoneNumber: isValidPhoneNumberMock, + normalizePhoneNumber: normalizePhoneNumberMock, +})); + +vi.mock('../../../src/utils/logger.js', () => ({ + default: () => loggerMock, +})); + +type MockUser = { + id: string; + email: string; + phone: string | null; + roles: string[]; + verified?: boolean; + emailVerified?: boolean; + phoneVerified?: boolean; + emailVerificationToken?: string; + emailVerificationTokenExpiry?: Date | null; + phoneVerificationToken?: string; + phoneVerificationTokenExpiry?: Date | null; + update: ReturnType; +}; + +function buildUser(overrides: Partial = {}): MockUser { + return { + id: 'user-1', + email: 'test@example.com', + phone: '+14155552671', + roles: ['user'], + verified: true, + emailVerified: true, + phoneVerified: true, + emailVerificationToken: 'EMAILOTP', + emailVerificationTokenExpiry: new Date(Date.now() + 60_000), + phoneVerificationToken: '123456', + phoneVerificationTokenExpiry: new Date(Date.now() + 60_000), + update: vi.fn(), + ...overrides, + }; +} + +function buildReq(user: MockUser, overrides: Record = {}) { + const headers: Record = { + ...(overrides.headers as Record | undefined), + }; + + return { + body: {}, + headers, + get: vi.fn((name: string) => headers[name.toLowerCase()] ?? headers[name] ?? undefined), + user, + ...overrides, + } as any; +} + +function buildRes() { + const res: any = {}; + res.status = vi.fn().mockReturnValue(res); + res.json = vi.fn().mockReturnValue(res); + return res; +} + +async function loadOtpController(authMode: 'web' | 'server' = 'server') { + vi.resetModules(); + vi.stubEnv('AUTH_MODE', authMode); + return import('../../../src/controllers/otp.js'); +} + +beforeEach(() => { + vi.clearAllMocks(); + vi.unstubAllEnvs(); + + signEphemeralTokenMock.mockResolvedValue('ephemeral-token'); + generatePhoneOTPMock.mockResolvedValue('654321'); + generateEmailOTPMock.mockResolvedValue('ABCDEF'); + isValidPhoneNumberMock.mockReturnValue(true); + normalizePhoneNumberMock.mockReturnValue('+14155552671'); + isValidEmailMock.mockReturnValue(true); +}); + +describe('otp controller', () => { + it('returns external phone OTP delivery payload in server mode', async () => { + const { sendPhoneOTP } = await loadOtpController('server'); + const user = buildUser(); + const req = buildReq(user, { + headers: { 'x-seamless-auth-delivery-mode': 'external' }, + }); + const res = buildRes(); + + await sendPhoneOTP(req, res); + + expect(generatePhoneOTPMock).toHaveBeenCalledWith(user, { sendMessage: false }); + expect(signEphemeralTokenMock).toHaveBeenCalledWith(user.id); + expect(setAuthCookiesMock).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith({ + message: 'success', + token: 'ephemeral-token', + delivery: { + kind: 'otp_sms', + to: '+14155552671', + token: '654321', + }, + }); + }); + + it('rejects phone OTP requests when the user phone is missing', async () => { + const { sendPhoneOTP } = await loadOtpController('server'); + const user = buildUser({ phone: null }); + const req = buildReq(user); + const res = buildRes(); + + await sendPhoneOTP(req, res); + + expect(authEventLogMock).toHaveBeenCalledWith( + expect.objectContaining({ + userId: user.id, + type: 'otp_suspicious', + }), + ); + expect(res.status).toHaveBeenCalledWith(400); + expect(res.json).toHaveBeenCalledWith({ error: 'Invalid data' }); + }); + + it('rejects email OTP requests when the email is invalid', async () => { + const { sendEmailOTP } = await loadOtpController('server'); + const user = buildUser({ email: 'bad-email' }); + const req = buildReq(user); + const res = buildRes(); + isValidEmailMock.mockReturnValue(false); + + await sendEmailOTP(req, res); + + expect(generateEmailOTPMock).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(400); + expect(res.json).toHaveBeenCalledWith({ error: 'Invalid data.' }); + }); + + it('sets cookies and returns external email delivery payload in web mode', async () => { + const { sendEmailOTP } = await loadOtpController('web'); + const user = buildUser(); + const req = buildReq(user, { + headers: { 'x-seamless-auth-delivery-mode': 'external' }, + }); + const res = buildRes(); + + await sendEmailOTP(req, res); + + expect(generateEmailOTPMock).toHaveBeenCalledWith(user, { sendMessage: false }); + expect(setAuthCookiesMock).toHaveBeenCalledWith(res, { ephemeralToken: 'ephemeral-token' }); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith({ + message: 'success', + delivery: { + kind: 'otp_email', + to: user.email, + token: 'ABCDEF', + }, + }); + }); + + it('returns 401 when phone verification data is missing', async () => { + const { verifyPhoneNumber } = await loadOtpController('server'); + const user = buildUser({ + phoneVerificationToken: undefined, + phoneVerificationTokenExpiry: null, + }); + const req = buildReq(user, { + body: { verificationToken: '123456' }, + }); + const res = buildRes(); + + await verifyPhoneNumber(req, res); + + expect(res.status).toHaveBeenCalledWith(401); + expect(res.json).toHaveBeenCalledWith({ error: 'Failed to verify OTP' }); + }); + + it('returns success without issuing a session when phone verification is partial', async () => { + const { verifyPhoneNumber } = await loadOtpController('server'); + const verifiedUser = buildUser({ + phoneVerified: true, + emailVerified: false, + verified: false, + }); + const req = buildReq(buildUser(), { + body: { verificationToken: '123456' }, + }); + const res = buildRes(); + + verifyPhoneOTPMock.mockResolvedValue({ + user: verifiedUser, + verified: true, + }); + + await verifyPhoneNumber(req, res); + + expect(issueSessionAndRespondMock).not.toHaveBeenCalled(); + expect(res.json).toHaveBeenCalledWith({ message: 'Success' }); + }); + + it('issues a session when login phone verification fully verifies the user', async () => { + const { verifyLoginPhoneNumber } = await loadOtpController('server'); + const verifiedUser = buildUser(); + const req = buildReq(buildUser(), { + body: { verificationToken: '123456' }, + }); + const res = buildRes(); + + verifyPhoneOTPMock.mockResolvedValue({ + user: verifiedUser, + verified: true, + }); + + await verifyLoginPhoneNumber(req, res); + + expect(issueSessionAndRespondMock).toHaveBeenCalledWith({ + user: { + id: verifiedUser.id, + email: verifiedUser.email, + phone: verifiedUser.phone, + roles: verifiedUser.roles, + }, + req, + res, + authMode: 'server', + }); + expect(verifiedUser.update).toHaveBeenCalledWith({ + lastLogin: expect.any(Date), + }); + }); + + it('returns 401 when login phone verification fails', async () => { + const { verifyLoginPhoneNumber } = await loadOtpController('server'); + const failingUser = buildUser(); + const req = buildReq(buildUser(), { + body: { verificationToken: '123456' }, + }); + const res = buildRes(); + + verifyPhoneOTPMock.mockResolvedValue({ + user: failingUser, + verified: false, + }); + + await verifyLoginPhoneNumber(req, res); + + expect(authEventLogMock).toHaveBeenCalledWith( + expect.objectContaining({ + userId: failingUser.id, + type: 'verify_otp_failed', + }), + ); + expect(res.status).toHaveBeenCalledWith(401); + expect(res.json).toHaveBeenCalledWith({ error: 'Not allowed' }); + }); + + it('issues a session when email verification fully verifies the user', async () => { + const { verifyEmail } = await loadOtpController('server'); + const verifiedUser = buildUser(); + const req = buildReq(buildUser(), { + body: { verificationToken: 'EMAILOTP' }, + }); + const res = buildRes(); + + verifyEmailOTPMock.mockResolvedValue({ + user: verifiedUser, + verified: true, + }); + + await verifyEmail(req, res); + + expect(issueSessionAndRespondMock).toHaveBeenCalledWith({ + user: { + id: verifiedUser.id, + email: verifiedUser.email, + phone: verifiedUser.phone, + roles: verifiedUser.roles, + }, + req, + res, + authMode: 'server', + }); + expect(verifiedUser.update).toHaveBeenCalledWith({ + lastLogin: expect.any(Date), + }); + }); + + it('returns 500 when login email verification fails after lookup succeeds', async () => { + const { verifyLoginEmail } = await loadOtpController('server'); + const failingUser = buildUser(); + const req = buildReq(buildUser(), { + body: { verificationToken: 'EMAILOTP' }, + }); + const res = buildRes(); + + verifyEmailOTPMock.mockResolvedValue({ + user: failingUser, + verified: false, + }); + + await verifyLoginEmail(req, res); + + expect(authEventLogMock).toHaveBeenCalledWith( + expect.objectContaining({ + userId: failingUser.id, + type: 'verify_otp_failed', + }), + ); + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ error: 'Internal server error' }); + }); +}); diff --git a/tests/unit/lib/defineRoute.spec.ts b/tests/unit/lib/defineRoute.spec.ts new file mode 100644 index 0000000..8b99180 --- /dev/null +++ b/tests/unit/lib/defineRoute.spec.ts @@ -0,0 +1,178 @@ +import { Router } from 'express'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { z } from 'zod'; + +vi.mock('../../../src/middleware/attachAuthMiddleware.js', () => ({ + attachAuthMiddleware: vi.fn((cookieType: 'access' | 'ephemeral' = 'access') => + Object.assign(vi.fn(), { seamlessAuthType: cookieType }), + ), + getSecuritySchemeName: vi.fn((cookieType: 'access' | 'ephemeral') => { + if (process.env.AUTH_MODE === 'server') { + return 'bearerAuth'; + } + + return cookieType === 'ephemeral' ? 'ephemeralCookieAuth' : 'accessCookieAuth'; + }), +})); + +vi.mock('../../../src/openapi/registry', () => ({ + registry: { + registerPath: vi.fn(), + }, +})); + +describe('defineRoute', () => { + beforeEach(() => { + vi.resetModules(); + vi.clearAllMocks(); + delete process.env.AUTH_MODE; + }); + + it('adds access cookie security when auth is inferred from middleware in web mode', async () => { + const { defineRoute } = await import('../../../src/lib/defineRoute'); + const { registry } = await import('../../../src/openapi/registry'); + const middleware = Object.assign(vi.fn(), { seamlessAuthType: 'access' as const }); + + defineRoute(Router(), { + method: 'get', + path: '/secure', + middleware: [middleware], + schemas: { + response: { + 200: z.object({ + message: z.string(), + }), + }, + }, + handler: vi.fn(), + }); + + expect(registry.registerPath).toHaveBeenCalledWith( + expect.objectContaining({ + security: [{ accessCookieAuth: [] }], + }), + ); + }); + + it('adds ephemeral cookie security when auth is inferred from middleware in web mode', async () => { + const { defineRoute } = await import('../../../src/lib/defineRoute'); + const { registry } = await import('../../../src/openapi/registry'); + const middleware = Object.assign(vi.fn(), { seamlessAuthType: 'ephemeral' as const }); + + defineRoute(Router(), { + method: 'get', + path: '/ephemeral', + middleware: [middleware], + schemas: { + response: { + 200: z.object({ + message: z.string(), + }), + }, + }, + handler: vi.fn(), + }); + + expect(registry.registerPath).toHaveBeenCalledWith( + expect.objectContaining({ + security: [{ ephemeralCookieAuth: [] }], + }), + ); + }); + + it('adds bearer security in server mode even when auth comes from middleware', async () => { + process.env.AUTH_MODE = 'server'; + + const { defineRoute } = await import('../../../src/lib/defineRoute'); + const { registry } = await import('../../../src/openapi/registry'); + const middleware = Object.assign(vi.fn(), { seamlessAuthType: 'access' as const }); + + defineRoute(Router(), { + method: 'get', + path: '/server-secure', + middleware: [middleware], + schemas: { + response: { + 200: z.object({ + message: z.string(), + }), + }, + }, + handler: vi.fn(), + }); + + expect(registry.registerPath).toHaveBeenCalledWith( + expect.objectContaining({ + security: [{ bearerAuth: [] }], + }), + ); + }); + + it('runs auth middleware before custom middleware when auth is declared on the route', async () => { + const order: string[] = []; + const { defineRoute } = await import('../../../src/lib/defineRoute'); + const { attachAuthMiddleware } = + await import('../../../src/middleware/attachAuthMiddleware.js'); + + (attachAuthMiddleware as any).mockImplementation((cookieType: 'access' | 'ephemeral') => + Object.assign( + (_req: any, _res: any, next: () => void) => { + order.push(`auth:${cookieType}`); + next(); + }, + { seamlessAuthType: cookieType }, + ), + ); + + const router = Router(); + + defineRoute(router, { + method: 'get', + path: '/ordered', + auth: 'access', + middleware: [ + (_req: any, _res: any, next: () => void) => { + order.push('custom'); + next(); + }, + ], + schemas: { + response: { + 200: z.object({ + message: z.string(), + }), + }, + }, + handler: (_req, res) => { + order.push('handler'); + res.status(200).json({ message: 'ok' }); + }, + }); + + const layer = (router as any).stack.find((entry: any) => entry.route?.path === '/ordered'); + const handlers = layer.route.stack.map((entry: any) => entry.handle); + const req = { params: {}, query: {}, body: {} }; + const res = { + statusCode: 200, + status(code: number) { + this.statusCode = code; + return this; + }, + json: vi.fn(), + }; + + const run = async (index: number): Promise => { + const handler = handlers[index]; + + if (!handler) { + return; + } + + await handler(req, res, () => run(index + 1)); + }; + + await run(0); + + expect(order).toEqual(['auth:access', 'custom', 'handler']); + }); +}); diff --git a/tests/unit/lib/token.spec.ts b/tests/unit/lib/token.spec.ts index db7a0ae..4898270 100644 --- a/tests/unit/lib/token.spec.ts +++ b/tests/unit/lib/token.spec.ts @@ -1,14 +1,12 @@ import { vi } from 'vitest'; -vi.unmock('../../../src/config/getSystemConfig'); vi.unmock('../../../src/lib/token'); -vi.unmock('../../../src/utils/signingKeyStore'); -vi.mock('../../../src/utils/signingKeyStore', () => ({ +vi.mock('../../../src/utils/signingKeyStore.js', () => ({ getSigningKey: vi.fn(), })); -vi.mock('../../../src/config/getSystemConfig', () => ({ +vi.mock('../../../src/config/getSystemConfig.js', () => ({ getSystemConfig: vi.fn(), })); @@ -38,6 +36,10 @@ vi.mock('jose', () => { }); vi.mock('crypto', () => ({ + createHmac: vi.fn(() => ({ + update: vi.fn().mockReturnThis(), + digest: vi.fn(() => 'lookup-hash'), + })), randomBytes: vi.fn(() => ({ toString: () => 'random-token', })), @@ -54,10 +56,13 @@ describe('token utils', () => { vi.resetModules(); vi.clearAllMocks(); process.env.ISSUER = 'issuer'; + delete process.env.REFRESH_TOKEN_LOOKUP_SECRET; + delete process.env.API_SERVICE_TOKEN; + delete process.env.APP_ID; + process.env.NODE_ENV = 'test'; }); - // TODO: issue when precommit running. - it.skip('signs access token', async () => { + it('signs access token', async () => { const { getSigningKey } = await import('../../../src/utils/signingKeyStore'); const { getSystemConfig } = await import('../../../src/config/getSystemConfig'); @@ -77,8 +82,7 @@ describe('token utils', () => { expect(result).toBe('mock-jwt'); }); - // TODO: issue when precommit running. - it.skip('signs refresh token', async () => { + it('signs refresh token', async () => { const { getSigningKey } = await import('../../../src/utils/signingKeyStore'); const { getSystemConfig } = await import('../../../src/config/getSystemConfig'); @@ -98,8 +102,7 @@ describe('token utils', () => { expect(result).toBe('mock-jwt'); }); - // TODO: issue when precommit running. - it.skip('signs ephemeral token', async () => { + it('signs ephemeral token', async () => { const { getSigningKey } = await import('../../../src/utils/signingKeyStore'); (getSigningKey as any).mockResolvedValue({ @@ -141,4 +144,40 @@ describe('token utils', () => { expect(result).toBe('hashed-token'); }); + + it('creates refresh token lookup fingerprints', async () => { + process.env.API_SERVICE_TOKEN = 'service-secret'; + const { createRefreshTokenLookup } = await import('../../../src/lib/token'); + + const result = createRefreshTokenLookup('token'); + + expect(result).toBe('lookup-hash'); + }); + + it('prefers an explicit refresh token lookup secret when configured', async () => { + process.env.REFRESH_TOKEN_LOOKUP_SECRET = 'lookup-secret'; + const { createRefreshTokenLookup } = await import('../../../src/lib/token'); + + const result = createRefreshTokenLookup('token'); + + expect(result).toBe('lookup-hash'); + }); + + it('uses the development fallback secret when no configured secret exists', async () => { + process.env.APP_ID = 'local-dev'; + const { createRefreshTokenLookup } = await import('../../../src/lib/token'); + + const result = createRefreshTokenLookup('token'); + + expect(result).toBe('lookup-hash'); + }); + + it('throws in production when no refresh token lookup secret is available', async () => { + process.env.NODE_ENV = 'production'; + const { createRefreshTokenLookup } = await import('../../../src/lib/token'); + + expect(() => createRefreshTokenLookup('token')).toThrow( + 'REFRESH_TOKEN_LOOKUP_SECRET (or API_SERVICE_TOKEN) must be set', + ); + }); }); diff --git a/tests/unit/middleware/attachAuthMiddleware.spec.ts b/tests/unit/middleware/attachAuthMiddleware.spec.ts index 1dbe698..af13272 100644 --- a/tests/unit/middleware/attachAuthMiddleware.spec.ts +++ b/tests/unit/middleware/attachAuthMiddleware.spec.ts @@ -5,24 +5,13 @@ vi.unmock('../../../src/middleware/verifyCookieAuth'); vi.unmock('../../../src/middleware/attachAuthMiddleware'); vi.mock('../../../src/middleware/verifyBearerAuth', () => ({ - verifyBearerAuth: () => vi.fn().mockResolvedValue('Bearer Auth User'), - verifyCookieAuth: () => vi.fn().mockResolvedValue('Cookie auth'), + verifyBearerAuth: vi.fn((_req: any, _res: any, next: any) => next()), })); vi.mock('../../../src/middleware/verifyCookieAuth', () => ({ - verifyCookieAuth: vi.fn(), + verifyCookieAuth: vi.fn(() => vi.fn((_req: any, _res: any, next: any) => next())), })); -vi.mock('../../src/middleware/attachAuthMiddleware.js', () => ({ - attachAuthMiddleware: (v: string) => (req: any, _res: any, next: any) => { - next(); - }, -})); - -import { attachAuthMiddleware } from '../../../src/middleware/attachAuthMiddleware'; -import { verifyCookieAuth } from '../../../src/middleware/verifyCookieAuth'; -import { verifyBearerAuth } from '../../../src/middleware/verifyBearerAuth'; - describe('attachAuthMiddleware', () => { beforeEach(() => { vi.resetModules(); @@ -33,15 +22,19 @@ describe('attachAuthMiddleware', () => { vi.unstubAllEnvs(); }); it('defaults to cookie auth', async () => { - attachAuthMiddleware(); + const { attachAuthMiddleware } = await import('../../../src/middleware/attachAuthMiddleware'); + const middleware = attachAuthMiddleware(); - expect(verifyCookieAuth).toHaveBeenCalledWith('access'); + expect(middleware.seamlessAuthType).toBe('access'); + expect(typeof middleware).toBe('function'); }); it('uses ephemeral cookie', async () => { - attachAuthMiddleware('ephemeral'); + const { attachAuthMiddleware } = await import('../../../src/middleware/attachAuthMiddleware'); + const middleware = attachAuthMiddleware('ephemeral'); - expect(verifyCookieAuth).toHaveBeenCalledWith('ephemeral'); + expect(middleware.seamlessAuthType).toBe('ephemeral'); + expect(typeof middleware).toBe('function'); }); it('uses bearer in server mode', async () => { diff --git a/tests/unit/openapi/document.spec.ts b/tests/unit/openapi/document.spec.ts index b387ed4..93b5156 100644 --- a/tests/unit/openapi/document.spec.ts +++ b/tests/unit/openapi/document.spec.ts @@ -58,3 +58,22 @@ describe('getPackageVersion', () => { expect(result).toBeDefined(); }); }); + +describe('generateOpenApiDocument', () => { + it('exposes the correct cookie auth schemes', async () => { + const { generateOpenApiDocument } = await import('../../../src/openapi/document'); + + const result = generateOpenApiDocument(); + + expect(result.components.securitySchemes.accessCookieAuth).toEqual({ + type: 'apiKey', + in: 'cookie', + name: 'seamless_access', + }); + expect(result.components.securitySchemes.ephemeralCookieAuth).toEqual({ + type: 'apiKey', + in: 'cookie', + name: 'seamless_ephemeral', + }); + }); +}); diff --git a/tests/unit/routes/auth.routes.spec.ts b/tests/unit/routes/auth.routes.spec.ts new file mode 100644 index 0000000..518119b --- /dev/null +++ b/tests/unit/routes/auth.routes.spec.ts @@ -0,0 +1,35 @@ +/* + * Copyright © 2026 Fells Code, LLC + * Licensed under the GNU Affero General Public License v3.0 + * See LICENSE file in the project root for full license information + */ + +import { describe, expect, it, vi } from 'vitest'; + +import authRouter from '../../../src/routes/auth.routes.js'; + +describe('auth routes', () => { + it('returns 405 for non-POST /refresh requests', () => { + const refreshLayers = (authRouter as any).stack.filter( + (layer: any) => layer.route?.path === '/refresh', + ); + + const methodNotAllowedLayer = refreshLayers.find((layer: any) => layer.route.methods._all); + + expect(methodNotAllowedLayer).toBeDefined(); + + const handler = methodNotAllowedLayer.route.stack[0].handle; + const req = {}; + const res = { + setHeader: vi.fn(), + status: vi.fn().mockReturnThis(), + json: vi.fn().mockReturnThis(), + }; + + handler(req, res); + + expect(res.setHeader).toHaveBeenCalledWith('Allow', 'POST'); + expect(res.status).toHaveBeenCalledWith(405); + expect(res.json).toHaveBeenCalledWith({ error: 'Method Not Allowed' }); + }); +}); diff --git a/tests/unit/services/authEventService.spec.ts b/tests/unit/services/authEventService.spec.ts index c369157..b75b765 100644 --- a/tests/unit/services/authEventService.spec.ts +++ b/tests/unit/services/authEventService.spec.ts @@ -1,14 +1,20 @@ import { vi } from 'vitest'; vi.unmock('../../../src/services/authEventService'); -vi.unmock('../../../src/models/authEvents'); -vi.unmock('../../../src/utils/logger'); - -vi.mock('../../../src/models/authEvents', () => ({ +vi.mock('../../../src/models/authEvents.js', () => ({ AuthEvent: { create: vi.fn(), }, })); +vi.mock('../../../src/utils/logger.js', () => ({ + default: vi.fn(() => ({ + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + debug: vi.fn(), + })), +})); + function buildReq(overrides: any = {}) { return { ip: '127.0.0.1', @@ -28,8 +34,8 @@ describe('AuthEventService', () => { }); it('logs event successfully', async () => { - const { AuthEvent } = await import('../../../src/models/authEvents'); - const { AuthEventService } = await import('../../../src/services/authEventService'); + const { AuthEvent } = await import('../../../src/models/authEvents.js'); + const { AuthEventService } = await import('../../../src/services/authEventService.js'); const req = buildReq(); @@ -49,8 +55,8 @@ describe('AuthEventService', () => { }); it('handles missing ip and user-agent', async () => { - const { AuthEvent } = await import('../../../src/models/authEvents'); - const { AuthEventService } = await import('../../../src/services/authEventService'); + const { AuthEvent } = await import('../../../src/models/authEvents.js'); + const { AuthEventService } = await import('../../../src/services/authEventService.js'); const req = { headers: {} } as any; @@ -67,11 +73,11 @@ describe('AuthEventService', () => { ); }); - it.skip('swallows errors and logs failure', async () => { - const { AuthEvent } = await import('../../../src/models/authEvents'); - const getLogger = (await import('../../../src/utils/logger')).default('test'); + it('swallows errors and logs failure', async () => { + const { AuthEvent } = await import('../../../src/models/authEvents.js'); + const getLogger = (await import('../../../src/utils/logger.js')).default; - const { AuthEventService } = await import('../../../src/services/authEventService'); + const { AuthEventService } = await import('../../../src/services/authEventService.js'); (AuthEvent.create as any).mockRejectedValue(new Error('fail')); @@ -82,11 +88,12 @@ describe('AuthEventService', () => { req, }); - expect(getLogger.error).toHaveBeenCalled(); + expect(getLogger).toHaveBeenCalledWith('authEventService'); + expect(getLogger.mock.results[0]?.value.error).toHaveBeenCalled(); }); it('loginSuccess calls log', async () => { - const { AuthEventService } = await import('../../../src/services/authEventService'); + const { AuthEventService } = await import('../../../src/services/authEventService.js'); const spy = vi.spyOn(AuthEventService, 'log'); @@ -102,7 +109,7 @@ describe('AuthEventService', () => { }); it('loginFailed includes reason', async () => { - const { AuthEventService } = await import('../../../src/services/authEventService'); + const { AuthEventService } = await import('../../../src/services/authEventService.js'); const spy = vi.spyOn(AuthEventService, 'log'); @@ -119,7 +126,7 @@ describe('AuthEventService', () => { }); it('tokenRotated calls log', async () => { - const { AuthEventService } = await import('../../../src/services/authEventService'); + const { AuthEventService } = await import('../../../src/services/authEventService.js'); const spy = vi.spyOn(AuthEventService, 'log'); @@ -136,7 +143,7 @@ describe('AuthEventService', () => { }); it('authActionTake calls log', async () => { - const { AuthEventService } = await import('../../../src/services/authEventService'); + const { AuthEventService } = await import('../../../src/services/authEventService.js'); const spy = vi.spyOn(AuthEventService, 'log'); @@ -153,7 +160,7 @@ describe('AuthEventService', () => { }); it('notificationSent calls log', async () => { - const { AuthEventService } = await import('../../../src/services/authEventService'); + const { AuthEventService } = await import('../../../src/services/authEventService.js'); const spy = vi.spyOn(AuthEventService, 'log'); @@ -165,7 +172,7 @@ describe('AuthEventService', () => { }); it('serviceTokenUsed logs correct metadata', async () => { - const { AuthEventService } = await import('../../../src/services/authEventService'); + const { AuthEventService } = await import('../../../src/services/authEventService.js'); const spy = vi.spyOn(AuthEventService, 'log'); @@ -181,7 +188,7 @@ describe('AuthEventService', () => { }); it('serviceTokenInvalid logs failure', async () => { - const { AuthEventService } = await import('../../../src/services/authEventService'); + const { AuthEventService } = await import('../../../src/services/authEventService.js'); const spy = vi.spyOn(AuthEventService, 'log'); diff --git a/tests/unit/services/messagingService.spec.ts b/tests/unit/services/messagingService.spec.ts index a1fed9a..af128fc 100644 --- a/tests/unit/services/messagingService.spec.ts +++ b/tests/unit/services/messagingService.spec.ts @@ -1,9 +1,30 @@ import { vi } from 'vitest'; +const sendOtpEmailMock = vi.fn(); +const sendOtpSmsMock = vi.fn(); +const sendMagicLinkEmailMock = vi.fn(); +const sendBootstrapInviteEmailMock = vi.fn(); +const createDirectAuthMessagingServiceMock = vi.fn(() => ({ + sendOtpEmail: sendOtpEmailMock, + sendOtpSms: sendOtpSmsMock, + sendMagicLinkEmail: sendMagicLinkEmailMock, + sendBootstrapInviteEmail: sendBootstrapInviteEmailMock, +})); + vi.unmock('../../../src/services/messagingService'); +vi.mock('../../../src/config/directMessaging', () => ({ + createDirectAuthMessagingService: createDirectAuthMessagingServiceMock, +})); +vi.mock('../../../src/config/getSystemConfig', () => ({ + getSystemConfig: vi.fn().mockResolvedValue({ + app_name: 'Seamless Auth Test', + }), +})); vi.mock('../../../src/utils/logger', () => ({ default: () => ({ debug: vi.fn(), + info: vi.fn(), + error: vi.fn(), }), })); @@ -13,6 +34,7 @@ describe('messagingService', () => { beforeEach(() => { vi.resetModules(); vi.clearAllMocks(); + process.env.MESSAGING_ENABLE_IN_DEV = ''; }); it('does nothing in development (email)', async () => { @@ -21,6 +43,7 @@ describe('messagingService', () => { const { sendOTPEmail } = await import('../../../src/services/messagingService'); await expect(sendOTPEmail('test@example.com', '123456')).resolves.toBeUndefined(); + expect(createDirectAuthMessagingServiceMock).not.toHaveBeenCalled(); }); it('does nothing in development (sms)', async () => { @@ -29,6 +52,7 @@ describe('messagingService', () => { const { sendOTPSMS } = await import('../../../src/services/messagingService'); await expect(sendOTPSMS('+123', 123456)).resolves.toBeUndefined(); + expect(createDirectAuthMessagingServiceMock).not.toHaveBeenCalled(); }); it('does nothing in development (magic link)', async () => { @@ -39,6 +63,22 @@ describe('messagingService', () => { await expect( sendMagicLinkEmail('test@example.com', 'token', 'http://safe'), ).resolves.toBeUndefined(); + expect(createDirectAuthMessagingServiceMock).not.toHaveBeenCalled(); + }); + + it('can execute direct delivery in development when enabled', async () => { + process.env.NODE_ENV = 'development'; + process.env.MESSAGING_ENABLE_IN_DEV = 'true'; + + const { sendOTPEmail } = await import('../../../src/services/messagingService'); + + await expect(sendOTPEmail('test@example.com', '123456')).resolves.toBeUndefined(); + + expect(createDirectAuthMessagingServiceMock).toHaveBeenCalledWith('Seamless Auth Test'); + expect(sendOtpEmailMock).toHaveBeenCalledWith({ + to: 'test@example.com', + token: '123456', + }); }); it('does not throw in production', async () => { @@ -47,5 +87,6 @@ describe('messagingService', () => { const { sendOTPEmail } = await import('../../../src/services/messagingService'); await expect(sendOTPEmail('test@example.com', '123')).resolves.toBeUndefined(); + expect(createDirectAuthMessagingServiceMock).toHaveBeenCalledWith('Seamless Auth Test'); }); }); diff --git a/tests/unit/services/sessionIssueService.spec.ts b/tests/unit/services/sessionIssueService.spec.ts index 2ef7b68..28a28af 100644 --- a/tests/unit/services/sessionIssueService.spec.ts +++ b/tests/unit/services/sessionIssueService.spec.ts @@ -5,6 +5,7 @@ import { issueSessionAndRespond } from '../../../src/services/sessionIssuance.js vi.mock('../../../src/lib/token.js', () => ({ generateRefreshToken: vi.fn(), hashRefreshToken: vi.fn(), + createRefreshTokenLookup: vi.fn(), signAccessToken: vi.fn(), })); @@ -34,7 +35,12 @@ vi.mock('../../../src/utils/utils.js', () => ({ // ---- Imports AFTER mocks ---- -import { generateRefreshToken, hashRefreshToken, signAccessToken } from '../../../src/lib/token.js'; +import { + createRefreshTokenLookup, + generateRefreshToken, + hashRefreshToken, + signAccessToken, +} from '../../../src/lib/token.js'; import { Session } from '../../../src/models/sessions.js'; import { setAuthCookies, clearAuthCookies } from '../../../src/lib/cookie.js'; @@ -71,6 +77,7 @@ beforeEach(() => { (generateRefreshToken as any).mockReturnValue('refresh-token'); (hashRefreshToken as any).mockResolvedValue('hashed-refresh'); + (createRefreshTokenLookup as any).mockReturnValue('refresh-lookup'); (signAccessToken as any).mockResolvedValue('access-token'); (Session.create as any).mockResolvedValue({ id: 'session-1' }); @@ -100,6 +107,7 @@ it('issues session in web mode and sets cookies', async () => { }); expect(Session.create).toHaveBeenCalled(); + expect(createRefreshTokenLookup).toHaveBeenCalledWith('refresh-token'); expect(signAccessToken).toHaveBeenCalled(); expect(setAuthCookies).toHaveBeenCalledWith(res, { diff --git a/tests/unit/services/sessionService.spec.ts b/tests/unit/services/sessionService.spec.ts index 0c394a0..21a48d2 100644 --- a/tests/unit/services/sessionService.spec.ts +++ b/tests/unit/services/sessionService.spec.ts @@ -5,6 +5,8 @@ vi.unmock('../../../src/services/sessionService'); vi.mock('../../../src/models/sessions', () => ({ Session: { findByPk: vi.fn(), + findOne: vi.fn(), + findAll: vi.fn(), }, })); @@ -22,11 +24,19 @@ vi.mock('../../../src/utils/signingKeyStore', () => ({ getPublicKeyByKid: vi.fn(), })); +vi.mock('../../../src/lib/token', () => ({ + createRefreshTokenLookup: vi.fn(), +})); + vi.mock('jose', () => ({ jwtVerify: vi.fn(), importSPKI: vi.fn(), })); +vi.mock('bcrypt-ts', () => ({ + compareSync: vi.fn(), +})); + vi.mock('jsonwebtoken', () => ({ default: { verify: vi.fn(), @@ -86,17 +96,19 @@ describe('sessionService', () => { expect(result).toBeNull(); }); - it.skip('returns parsed access token', async () => { - const { verifyJwtWithKid } = await import('../../../src/services/sessionService'); + it('returns parsed access token', async () => { + const jose = await import('jose'); + const { getPublicKeyByKid } = await import('../../../src/utils/signingKeyStore'); - vi.spyOn( - await import('../../../src/services/sessionService'), - 'verifyJwtWithKid', - ).mockResolvedValue({ - sub: 'user', - sid: 'session', - roles: ['admin'], - } as any); + (getPublicKeyByKid as any).mockResolvedValue('pem'); + (jose.jwtVerify as any).mockResolvedValue({ + payload: { + typ: 'access', + sub: 'user', + sid: 'session', + roles: ['admin'], + }, + }); const { validateAccessToken } = await import('../../../src/services/sessionService'); @@ -110,11 +122,16 @@ describe('sessionService', () => { }); it('returns null if payload invalid', async () => { - const mod = await import('../../../src/services/sessionService'); + const jose = await import('jose'); + const { getPublicKeyByKid } = await import('../../../src/utils/signingKeyStore'); + const { validateAccessToken } = await import('../../../src/services/sessionService'); - vi.spyOn(mod, 'verifyJwtWithKid').mockResolvedValue(null); + (getPublicKeyByKid as any).mockResolvedValue('pem'); + (jose.jwtVerify as any).mockResolvedValue({ + payload: { typ: 'access', sub: 'user' }, + }); - const result = await mod.validateAccessToken('token'); + const result = await validateAccessToken('token'); expect(result).toBeNull(); }); @@ -157,6 +174,98 @@ describe('sessionService', () => { expect(result).toBe(session); }); + it('finds refresh sessions by indexed lookup before falling back to bcrypt comparison', async () => { + const { Session } = await import('../../../src/models/sessions'); + const { createRefreshTokenLookup } = await import('../../../src/lib/token'); + + const session = buildSession(); + + (createRefreshTokenLookup as any).mockReturnValue('lookup'); + (Session.findOne as any).mockResolvedValue(session); + + const { findRefreshSessionByToken } = await import('../../../src/services/sessionService'); + + const result = await findRefreshSessionByToken('refresh-token'); + + expect(Session.findOne).toHaveBeenCalledWith( + expect.objectContaining({ + where: expect.objectContaining({ + refreshTokenLookup: 'lookup', + }), + }), + ); + expect(result).toEqual({ + session, + legacyFallbackCandidates: 0, + usedLegacyFallback: false, + }); + }); + + it('falls back to legacy refresh-token hashes for sessions without lookup fingerprints', async () => { + const { Session } = await import('../../../src/models/sessions'); + const { createRefreshTokenLookup } = await import('../../../src/lib/token'); + const { compareSync } = await import('bcrypt-ts'); + + const legacySession = buildSession({ refreshTokenHash: 'legacy-hash' }); + + (createRefreshTokenLookup as any).mockReturnValue('lookup'); + (Session.findOne as any).mockResolvedValue(null); + (Session.findAll as any).mockResolvedValue([legacySession]); + (compareSync as any).mockReturnValue(true); + + const { findRefreshSessionByToken } = await import('../../../src/services/sessionService'); + + const result = await findRefreshSessionByToken('refresh-token'); + + expect(Session.findAll).toHaveBeenCalledWith( + expect.objectContaining({ + where: expect.objectContaining({ + refreshTokenLookup: null, + }), + }), + ); + expect(compareSync).toHaveBeenCalledWith('refresh-token', 'legacy-hash'); + expect(result).toEqual({ + session: legacySession, + legacyFallbackCandidates: 1, + usedLegacyFallback: true, + }); + }); + + it('returns a legacy fallback miss when no legacy session hash matches', async () => { + const { Session } = await import('../../../src/models/sessions'); + const { createRefreshTokenLookup } = await import('../../../src/lib/token'); + const { compareSync } = await import('bcrypt-ts'); + + (createRefreshTokenLookup as any).mockReturnValue('lookup'); + (Session.findOne as any).mockResolvedValue(null); + (Session.findAll as any).mockResolvedValue([buildSession({ refreshTokenHash: 'legacy-hash' })]); + (compareSync as any).mockReturnValue(false); + + const { findRefreshSessionByToken } = await import('../../../src/services/sessionService'); + + const result = await findRefreshSessionByToken('refresh-token'); + + expect(result).toEqual({ + session: null, + legacyFallbackCandidates: 1, + usedLegacyFallback: true, + }); + }); + + it('revokes replaced sessions during validateSessionRecord', async () => { + const { Session } = await import('../../../src/models/sessions'); + const mod = await import('../../../src/services/sessionService'); + + const session = buildSession({ replacedBySessionId: 'next-session' }); + (Session.findByPk as any).mockResolvedValue(session); + + const result = await mod.validateSessionRecord('id'); + + expect(session.save).toHaveBeenCalled(); + expect(result).toBeNull(); + }); + it('revokes chain', async () => { const { Session } = await import('../../../src/models/sessions'); diff --git a/tests/unit/utils/signingKeyStore.spec.ts b/tests/unit/utils/signingKeyStore.spec.ts index d6d8c1c..846d5aa 100644 --- a/tests/unit/utils/signingKeyStore.spec.ts +++ b/tests/unit/utils/signingKeyStore.spec.ts @@ -1,39 +1,50 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; - -function setupMocks() { - vi.mock('fs', async () => { - const actual = await vi.importActual('fs'); - return { - ...actual, - existsSync: vi.fn(), - readFileSync: vi.fn(), - mkdirSync: vi.fn(), - writeFileSync: vi.fn(), - }; - }); - - vi.mock('crypto', async () => { - const actual = await vi.importActual('crypto'); - return { +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('fs', () => ({ + default: { + existsSync: vi.fn(), + readFileSync: vi.fn(), + mkdirSync: vi.fn(), + writeFileSync: vi.fn(), + }, + existsSync: vi.fn(), + readFileSync: vi.fn(), + mkdirSync: vi.fn(), + writeFileSync: vi.fn(), +})); + +vi.mock('crypto', async () => { + const actual = await vi.importActual('crypto'); + return { + ...actual, + default: { ...actual, generateKeyPairSync: vi.fn(), - }; - }); + }, + generateKeyPairSync: vi.fn(), + }; +}); + +vi.mock('../../../src/utils/secretsStore.js', () => ({ + getSecret: vi.fn(), +})); - vi.mock('../../../src/utils/secretsStore', () => ({ - getSecret: vi.fn(), - })); -} +vi.mock('../../../src/utils/logger.js', () => ({ + default: vi.fn(() => ({ + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + })), +})); describe('signingKeyStore', () => { beforeEach(() => { vi.resetModules(); vi.clearAllMocks(); - setupMocks(); }); - //TODO: Come back and figure out these tests - describe.skip('DEV mode', () => { + describe('DEV mode', () => { it('generates dev key if none exists', async () => { process.env.NODE_ENV = 'development'; @@ -41,16 +52,22 @@ describe('signingKeyStore', () => { const crypto = await import('crypto'); (fs.existsSync as any).mockReturnValue(false); - + (fs.default.existsSync as any).mockReturnValue(false); (crypto.generateKeyPairSync as any).mockReturnValue({ privateKey: 'PRIVATE_KEY', publicKey: 'PUBLIC_KEY', }); + (crypto.default.generateKeyPairSync as any).mockReturnValue({ + privateKey: 'PRIVATE_KEY', + publicKey: 'PUBLIC_KEY', + }); - const { getSigningKey } = await import('../../../src/utils/signingKeyStore'); + const { getSigningKey } = await import('../../../src/utils/signingKeyStore.js'); const result = await getSigningKey(); + expect(fs.default.mkdirSync).toHaveBeenCalled(); + expect(fs.default.writeFileSync).toHaveBeenCalledTimes(2); expect(result.privateKeyPem).toBe('PRIVATE_KEY'); }); @@ -59,10 +76,12 @@ describe('signingKeyStore', () => { const fs = await import('fs'); - (fs.existsSync as any).mockReturnValue(true); + (fs.existsSync as any).mockReturnValueOnce(true).mockReturnValueOnce(true); + (fs.default.existsSync as any).mockReturnValueOnce(true).mockReturnValueOnce(true); (fs.readFileSync as any).mockReturnValue('EXISTING_KEY'); + (fs.default.readFileSync as any).mockReturnValue('EXISTING_KEY'); - const { getSigningKey } = await import('../../../src/utils/signingKeyStore'); + const { getSigningKey } = await import('../../../src/utils/signingKeyStore.js'); const result = await getSigningKey(); @@ -75,9 +94,11 @@ describe('signingKeyStore', () => { const fs = await import('fs'); (fs.existsSync as any).mockReturnValue(true); + (fs.default.existsSync as any).mockReturnValue(true); (fs.readFileSync as any).mockReturnValue('PUBLIC_KEY'); + (fs.default.readFileSync as any).mockReturnValue('PUBLIC_KEY'); - const { getPublicKeyByKid } = await import('../../../src/utils/signingKeyStore'); + const { getPublicKeyByKid } = await import('../../../src/utils/signingKeyStore.js'); const result = await getPublicKeyByKid('dev-main'); @@ -89,8 +110,9 @@ describe('signingKeyStore', () => { const fs = await import('fs'); (fs.existsSync as any).mockReturnValue(false); + (fs.default.existsSync as any).mockReturnValue(false); - const { getPublicKeyByKid } = await import('../../../src/utils/signingKeyStore'); + const { getPublicKeyByKid } = await import('../../../src/utils/signingKeyStore.js'); const result = await getPublicKeyByKid('dev-main'); expect(result).toBeNull(); @@ -101,13 +123,11 @@ describe('signingKeyStore', () => { it('loads signing key from secrets', async () => { process.env.NODE_ENV = 'production'; - const { getSecret } = await import('../../../src/utils/secretsStore'); + const { getSecret } = await import('../../../src/utils/secretsStore.js'); - (getSecret as any) - .mockResolvedValueOnce('kid-1') // ACTIVE_KID - .mockResolvedValueOnce('PRIVATE_KEY'); // private key + (getSecret as any).mockResolvedValueOnce('kid-1').mockResolvedValueOnce('PRIVATE_KEY'); - const { getSigningKey } = await import('../../../src/utils/signingKeyStore'); + const { getSigningKey } = await import('../../../src/utils/signingKeyStore.js'); const result = await getSigningKey(); @@ -118,22 +138,22 @@ describe('signingKeyStore', () => { it('caches signing key', async () => { process.env.NODE_ENV = 'production'; - const { getSecret } = await import('../../../src/utils/secretsStore'); + const { getSecret } = await import('../../../src/utils/secretsStore.js'); (getSecret as any).mockResolvedValueOnce('kid-1').mockResolvedValueOnce('PRIVATE_KEY'); - const { getSigningKey } = await import('../../../src/utils/signingKeyStore'); + const { getSigningKey } = await import('../../../src/utils/signingKeyStore.js'); await getSigningKey(); await getSigningKey(); - expect(getSecret).toHaveBeenCalledTimes(2); // only first load + expect(getSecret).toHaveBeenCalledTimes(2); }); it('loads public keys and retrieves by kid', async () => { process.env.NODE_ENV = 'production'; - const { getSecret } = await import('../../../src/utils/secretsStore'); + const { getSecret } = await import('../../../src/utils/secretsStore.js'); (getSecret as any).mockResolvedValue( JSON.stringify({ @@ -141,7 +161,7 @@ describe('signingKeyStore', () => { }), ); - const { getPublicKeyByKid } = await import('../../../src/utils/signingKeyStore'); + const { getPublicKeyByKid } = await import('../../../src/utils/signingKeyStore.js'); const result = await getPublicKeyByKid('k1'); @@ -151,11 +171,11 @@ describe('signingKeyStore', () => { it('returns null if public key not found', async () => { process.env.NODE_ENV = 'production'; - const { getSecret } = await import('../../../src/utils/secretsStore'); + const { getSecret } = await import('../../../src/utils/secretsStore.js'); (getSecret as any).mockResolvedValue(JSON.stringify({ keys: [] })); - const { getPublicKeyByKid } = await import('../../../src/utils/signingKeyStore'); + const { getPublicKeyByKid } = await import('../../../src/utils/signingKeyStore.js'); const result = await getPublicKeyByKid('missing'); diff --git a/validateEnvs.sh b/validateEnvs.sh old mode 100755 new mode 100644 index d433f2f..06b90c6 --- a/validateEnvs.sh +++ b/validateEnvs.sh @@ -1,14 +1,105 @@ #!/bin/sh -set -eu pipefail +set -eu -required_vars="APP_NAME APP_ID APP_ORIGINS ISSUER AUTH_MODE DB_HOST DB_PORT DB_USER DB_PASSWORD DB_NAME DEFAULT_ROLES AVAILABLE_ROLES DB_LOGGING ACCESS_TOKEN_TTL REFRESH_TOKEN_TTL RATE_LIMIT DELAY_AFTER API_SERVICE_TOKEN JWKS_ACTIVE_KID RPID ORIGINS" +require_var() { + var_name="$1" + var_value="$(eval "printf '%s' \"\${$var_name:-}\"")" -for var in $required_vars; do - if [ -z "$(eval echo \$$var)" ]; then - echo "Environment variable $var is not set" + if [ -z "$var_value" ]; then + echo "Environment variable $var_name is not set" exit 1 fi -done +} + +warn() { + echo "Warning: $1" +} + +run_migrations() { + if [ "${DB_LOGGING:-false}" = "true" ]; then + npx sequelize-cli db:migrate --debug + else + npx sequelize-cli db:migrate + fi +} + +require_var APP_NAME +require_var APP_ID +require_var APP_ORIGINS +require_var ISSUER +require_var AUTH_MODE +require_var DEFAULT_ROLES +require_var AVAILABLE_ROLES +require_var DB_LOGGING +require_var ACCESS_TOKEN_TTL +require_var REFRESH_TOKEN_TTL +require_var RATE_LIMIT +require_var DELAY_AFTER +require_var RPID +require_var ORIGINS + +if [ -n "${DATABASE_URL:-}" ]; then + echo "Using DATABASE_URL for database connectivity" +else + require_var DB_HOST + require_var DB_PORT + require_var DB_USER + require_var DB_NAME +fi + +if [ "${AUTH_MODE:-}" = "server" ]; then + require_var API_SERVICE_TOKEN +fi + +if [ "${SEAMLESS_BOOTSTRAP_ENABLED:-false}" = "true" ]; then + require_var SEAMLESS_BOOTSTRAP_SECRET +fi + +if [ "${NODE_ENV:-development}" = "production" ]; then + require_var SEAMLESS_JWKS_ACTIVE_KID + require_var JWKS_PUBLIC_KEYS + + active_kid="${SEAMLESS_JWKS_ACTIVE_KID}" + private_key_var="SEAMLESS_JWKS_KEY_${active_kid}_PRIVATE" + require_var "$private_key_var" +fi + +aws_region="${MESSAGING_AWS_REGION:-${AWS_REGION:-${REGION:-}}}" +email_from="${MESSAGING_EMAIL_FROM:-${SES_EMAIL:-}}" +sms_provider="$(printf '%s' "${MESSAGING_SMS_PROVIDER:-${SMS_PROVIDER:-}}" | tr '[:upper:]' '[:lower:]')" +sms_from="${MESSAGING_SMS_FROM:-${TWILIO_PHONE_NUMBER:-}}" +twilio_account_sid="${MESSAGING_TWILIO_ACCOUNT_SID:-${TWILIO_ACCOUNT_SID:-}}" +twilio_auth_token="${MESSAGING_TWILIO_AUTH_TOKEN:-${TWILIO_AUTH_TOKEN:-}}" + +if [ -n "$email_from" ] && [ -z "$aws_region" ]; then + echo "Environment variable MESSAGING_AWS_REGION or AWS_REGION is required when MESSAGING_EMAIL_FROM is set" + exit 1 +fi + +case "$sms_provider" in + "") + ;; + aws) + if [ -z "$aws_region" ]; then + echo "Environment variable MESSAGING_AWS_REGION or AWS_REGION is required when MESSAGING_SMS_PROVIDER=aws" + exit 1 + fi + ;; + twilio) + if [ -z "$twilio_account_sid" ] || [ -z "$twilio_auth_token" ] || [ -z "$sms_from" ]; then + echo "MESSAGING_TWILIO_ACCOUNT_SID, MESSAGING_TWILIO_AUTH_TOKEN, and MESSAGING_SMS_FROM are required when MESSAGING_SMS_PROVIDER=twilio" + exit 1 + fi + ;; + *) + echo "Unsupported MESSAGING_SMS_PROVIDER: $sms_provider" + exit 1 + ;; +esac + +if [ "${NODE_ENV:-development}" = "production" ] && [ -z "$email_from" ] && [ -z "$sms_provider" ]; then + warn "Direct email/SMS delivery is not configured. This is fine when using external delivery mode via a SeamlessAuth server adapter." +fi echo "Generating JWKS keys" node ./dist/scripts/initKeys.js @@ -16,12 +107,12 @@ echo "JWKS keys ready" echo "Running migrations..." -if ! npx sequelize-cli db:migrate --debug; then +if ! run_migrations; then echo "Initial migration failed. Attempting database creation..." if npm run db:create; then echo "Database created. Retrying migrations..." - npx sequelize-cli db:migrate --debug + run_migrations else echo "Database creation failed" exit 1