diff --git a/README.md b/README.md index d93bea1d7..fc4e50626 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ A set of fastify libraries - @prefabs.tech/fastify-config (https://www.npmjs.com/package/@prefabs.tech/fastify-config) - @prefabs.tech/fastify-graphql (https://www.npmjs.com/package/@prefabs.tech/fastify-graphql) - @prefabs.tech/fastify-mailer (https://www.npmjs.com/package/@prefabs.tech/fastify-mailer) +- @prefabs.tech/fastify-phone-auth (https://www.npmjs.com/package/@prefabs.tech/fastify-phone-auth) - @prefabs.tech/fastify-s3 (https://www.npmjs.com/package/@prefabs.tech/fastify-s3) - @prefabs.tech/fastify-slonik (https://www.npmjs.com/package/@prefabs.tech/fastify-slonik) - @prefabs.tech/fastify-user (https://www.npmjs.com/package/@prefabs.tech/fastify-user) diff --git a/packages/phone-auth/.gitignore b/packages/phone-auth/.gitignore new file mode 100644 index 000000000..62853f374 --- /dev/null +++ b/packages/phone-auth/.gitignore @@ -0,0 +1,4 @@ +**/*.log* +/coverage +/dist +/node_modules diff --git a/packages/phone-auth/FEATURES.md b/packages/phone-auth/FEATURES.md new file mode 100644 index 000000000..14b6238ba --- /dev/null +++ b/packages/phone-auth/FEATURES.md @@ -0,0 +1,75 @@ + + +# @prefabs.tech/fastify-phone-auth — Features + +## Plugin Lifecycle + +1. **Enable/disable via config flag** — when `config.phoneAuth.enabled === false`, no recipe factory is contributed and the SuperTokens passwordless endpoints are not served. The check is `=== false`; `undefined` means enabled. + +2. **Automatic recipe registration** — on registration (when enabled), the plugin pushes `initPasswordlessRecipe` into the SuperTokens recipe registry via `addSupertokensRecipe` from `@prefabs.tech/fastify-user`. No consumer wiring beyond registering the plugin is required. + +3. **Registration order guard** — `addSupertokensRecipe` throws when the Fastify instance already carries the `supertokensInitialized` decorator, i.e. when this plugin is registered *after* `@prefabs.tech/fastify-user`. SuperTokens allows exactly one global `init()`, so a late registration could not contribute a recipe; failing loudly beats silently dropping passwordless login. + +4. **Phone number migration** — on `onReady` the plugin runs an idempotent `ALTER TABLE ... ADD COLUMN IF NOT EXISTS phone_number VARCHAR ( 20 )` against `config.user.tables?.users?.name` (default `users`). It runs on `onReady` rather than at registration because the users table is created while `@prefabs.tech/fastify-user` registers, and this plugin must be registered before that one. No migration runs when `enabled === false`. + +5. **`User` type augmentation** — the package augments the `User` interface from `@prefabs.tech/fastify-user` with `phoneNumber?: string`, which also flows into `UserCreateInput`. `@prefabs.tech/fastify-user` does not carry the field in its REST response schema, so it is not serialized on the users REST routes; GraphQL exposure is handled at runtime, below. + +6. **Runtime GraphQL `User` extension** — in the same `onReady` hook, the plugin calls `fastify.graphql.extendSchema("extend type User { phoneNumber: String }")`, so the field appears on the GraphQL `User` type with no consumer wiring. It is guarded by `fastify.graphql?.schema?.getType("User")`: apps without GraphQL enabled, or that never merged `userSchema`, are skipped rather than failed. No resolver is required — the default field resolver reads the camelized `phoneNumber` off the row. + +7. **No routes of its own** — this package registers no controllers. The passwordless endpoints are served by the SuperTokens Fastify plugin that `@prefabs.tech/fastify-user` registers. + +## Recipe Configuration + +8. **Default contact method and flow type** — `contactMethod` defaults to `"PHONE"` and `flowType` to `"USER_INPUT_CODE"`, both overridable through `config.phoneAuth`. `flowType` is typed to `"USER_INPUT_CODE"` only; magic-link flows are not supported. + +9. **Full recipe escape hatch** — when `config.phoneAuth.recipe` is a function, it is called with the Fastify instance and its return value is passed straight to `Passwordless.init`, bypassing the generated config entirely. + +10. **Boot-time config validation** — `getPasswordlessRecipeConfig` throws when `config.phoneAuth` is absent, when `enableDevMode` is true without a `devModeOtp`, and (outside dev mode) when the Twilio credentials are missing or incomplete. All three run inside `supertokens.init()`, so they fail at boot. + +11. **API override wrappers** — each entry in `config.phoneAuth.override.apis` is invoked with `(originalImplementation, fastify)` and spread over the built-in API overrides, so a consumer wrapper wins. + +12. **Function override wrappers** — same mechanism for `config.phoneAuth.override.functions` over the built-in `consumeCode` override. + +## Twilio Verify Integration + +13. **Placeholder user input code** — `getCustomUserInputCode` returns `TWILIO_VERIFY_PLACEHOLDER_CODE` (`"000000"`) for regular numbers, so SuperTokens stores a code while Twilio Verify owns the real OTP. + +14. **Dev mode OTP** — when `config.phoneAuth.enableDevMode` is true, `getCustomUserInputCode` returns `devModeOtp` for every number. + +15. **Per-number SMS bypass** — outside dev mode, numbers listed in `config.phoneAuth.bypassSmsFor` also get `devModeOtp` and no SMS is sent. + +16. **SMS delivery through Twilio Verify** — outside dev mode, `smsDelivery.override.sendSms` calls `verify.v2.services(verifyServiceSid).verifications.create({ channel: "sms", to })`. Send failures are logged and rethrown. + +17. **Dev mode skips SMS delivery entirely** — in dev mode the recipe supplies `createAndSendCustomTextMessage` (a log line) instead of `smsDelivery`. + +18. **Phone number capture on create** — the `createCodePOST` override copies `input.phoneNumber` onto `input.userContext` so downstream hooks can read it. + +19. **OTP verification on consume** — the `consumeCodePOST` override looks the device up by `preAuthSessionId`, then calls `verify.v2.services(verifyServiceSid).verificationChecks.create({ code, to })`. On `approved` it replays the original `consumeCodePOST` with the placeholder code; otherwise it returns `INCORRECT_USER_INPUT_CODE_ERROR`. + +20. **Graceful degradation to RESTART_FLOW_ERROR** — a missing device/phone number, unusable Twilio credentials, or a thrown Twilio Verify call all return `{ status: "RESTART_FLOW_ERROR" }` after logging. + +21. **Dev mode and bypassed numbers skip Twilio on consume** — they go straight to the original `consumeCodePOST`, which validates against `devModeOtp`. + +22. **Magic link flows pass through untouched** — when `input` carries no `userInputCode`, `consumeCodePOST` delegates to the original implementation without contacting Twilio. + +23. **Synthetic email enrichment** — successful consume responses get `email` filled in as `@` when SuperTokens has none. + +## Local User Creation + +24. **Role existence check before signup** — `functions.consumeCode` verifies every role in `userContext.roles` (default `[config.user.role ?? ROLE_USER]`) exists, throwing a `SIGNUP_FAILED_ERROR` `CustomError` otherwise. + +25. **Local user row on first sign-in** — when SuperTokens reports `createdNewUser`, a row is created through `getUserService` with the id, phone number, and synthetic email. The email domain falls back to the app name lowercased with whitespace stripped plus `.com`. + +26. **Rollback on failed insert** — if the local insert throws, the SuperTokens user is deleted via `deleteUser` before the error is rethrown, so the two stores cannot drift. + +27. **Missing phone number aborts signup** — when neither a phone number nor an email is available the SuperTokens user is deleted and an error is thrown. + +28. **Role assignment** — each role is assigned with `UserRoles.addRoleToUser`; a non-`OK` status is logged rather than thrown. + +29. **`lastLoginAt` refresh on returning users** — when no new user was created, `lastLoginAt` is updated; a failure is logged and swallowed so sign-in still succeeds. + +30. **Multi-tenant request context** — the user service is built from the request recovered via `getRequestFromUserContext`, so `request.config`, `request.slonik` and `request.dbSchema` win over the Fastify-level ones when present. + +## Known Limitations + +31. **`bypassSmsFor` does not apply on resend** — `resendCodePOST` is not overridden and `userContext.phoneNumber` is only set by `createCodePOST`, so `getCustomUserInputCode` cannot match a bypassed number on the resend path. diff --git a/packages/phone-auth/GUIDE.md b/packages/phone-auth/GUIDE.md new file mode 100644 index 000000000..f075163f9 --- /dev/null +++ b/packages/phone-auth/GUIDE.md @@ -0,0 +1,235 @@ +# @prefabs.tech/fastify-phone-auth — Developer Guide + +## Installation + +### For package consumers + +```bash +npm install @prefabs.tech/fastify-phone-auth +``` + +```bash +pnpm add @prefabs.tech/fastify-phone-auth +``` + +Peer dependencies are listed in [README.md](./README.md#requirements). + +### For monorepo development + +```bash +pnpm install +pnpm --filter @prefabs.tech/fastify-phone-auth test +pnpm --filter @prefabs.tech/fastify-phone-auth build +``` + +## Registration order — read this first + +SuperTokens permits exactly one global `supertokens.init()`. `@prefabs.tech/fastify-user` performs it synchronously while it is being registered, building its recipe list at that moment. Recipe plugins therefore contribute their recipe through a registry that `@prefabs.tech/fastify-user` drains at init time, which means **this plugin must be registered before it**. + +```typescript +await fastify.register(configPlugin, { config }); +await fastify.register(slonikPlugin); +await fastify.register(phoneAuthPlugin); // pushes the recipe factory +await fastify.register(userPlugin); // supertokens.init() drains the registry +``` + +Get the order wrong and registration fails loudly rather than silently dropping passwordless login: + +``` +Error: SuperTokens is already initialised. Register SuperTokens recipe plugins +before @prefabs.tech/fastify-user. +``` + +The registry itself is `addSupertokensRecipe`, exported from `@prefabs.tech/fastify-user`. It is generic — any package can use it to contribute a SuperTokens recipe. + +## Setup + +```typescript +import type { ApiConfig } from "@prefabs.tech/fastify-config"; + +import configPlugin from "@prefabs.tech/fastify-config"; +import phoneAuthPlugin from "@prefabs.tech/fastify-phone-auth"; +import slonikPlugin from "@prefabs.tech/fastify-slonik"; +import userPlugin from "@prefabs.tech/fastify-user"; +import Fastify from "fastify"; + +const config: ApiConfig = { + // ...the rest of your app config + phoneAuth: { + fallbackEmailDomain: "example.com", + twilio: { + accountSid: process.env.TWILIO_ACCOUNT_SID as string, + authToken: process.env.TWILIO_AUTH_TOKEN as string, + verifyServiceSid: process.env.TWILIO_VERIFY_SERVICE_SID as string, + }, + }, +}; + +const fastify = Fastify(); + +await fastify.register(configPlugin, { config }); +await fastify.register(slonikPlugin); +await fastify.register(phoneAuthPlugin); +await fastify.register(userPlugin); +``` + +All subsequent examples assume this setup. + +--- + +## Base Libraries + +### `supertokens-node` — Passwordless recipe (MODIFIED passthrough) + +This plugin does not expose routes of its own. It configures SuperTokens' Passwordless recipe, and the SuperTokens Fastify plugin registered by `@prefabs.tech/fastify-user` serves the resulting endpoints (`POST /signinup/code`, `POST /signinup/code/consume`, `POST /signinup/code/resend`). See the [SuperTokens Passwordless docs](https://supertokens.com/docs/passwordless/introduction) for the endpoint contracts. + +Our delta over the stock recipe: + +- `contactMethod` is constrained to `"EMAIL" | "EMAIL_OR_PHONE" | "PHONE"` and defaults to `"PHONE"`. +- `flowType` is constrained to `"USER_INPUT_CODE"` — magic-link and link-or-code flows are deliberately not supported. +- `getCustomUserInputCode` returns a placeholder rather than a real OTP (see below). +- `apis.consumeCodePOST`, `apis.createCodePOST` and `functions.consumeCode` are overridden. `resendCodePOST` and `functions.createCode` are not. +- `smsDelivery.sendSms` is replaced with a Twilio Verify call, or with a log line in dev mode. + +### `twilio` — Verify API (PARTIAL passthrough) + +Only the Verify v2 service is used: `verifications.create` to send an OTP and `verificationChecks.create` to check one. Messaging/SMS APIs are not used, which is why `TwilioConfig` omits `from` and `messagingServiceSid` and requires `verifyServiceSid` instead. + +--- + +## How the Twilio Verify bridge works + +SuperTokens insists on owning a user input code; Twilio Verify insists on owning the OTP. The two are reconciled like this: + +1. Sign in/up hits `createCodePOST`. The override records the phone number on `userContext`, then the SMS-delivery override asks Twilio Verify to send an OTP. +2. SuperTokens still stores a code of its own, so `getCustomUserInputCode` hands it the constant `TWILIO_VERIFY_PLACEHOLDER_CODE` (`"000000"`) instead of the real OTP. +3. The user submits the OTP they received. `consumeCodePOST` looks the device up by `preAuthSessionId` to recover the phone number, then checks the submitted code against Twilio Verify. If Twilio approves, the original `consumeCodePOST` is replayed with the placeholder so SuperTokens can complete its own flow. +4. `functions.consumeCode` then creates the matching row in your `users` table. + +## User creation + +On first successful sign-in, `functions.consumeCode`: + +- Verifies every role in `userContext.roles` (default `[config.user.role ?? "USER"]`) exists, throwing a `SIGNUP_FAILED_ERROR` `CustomError` otherwise. +- Creates the local user with the phone number and a synthetic email of `@`, falling back to `.com` when `fallbackEmailDomain` is unset. SuperTokens requires an email; passwordless phone users do not supply one. +- Assigns the roles via `UserRoles.addRoleToUser`. +- Deletes the SuperTokens user again if the local insert fails, so the two stores cannot drift. + +On subsequent sign-ins it only updates `lastLoginAt`. + +## Migration + +This package owns the `phone_number` column. On `onReady` it runs an idempotent + +```sql +ALTER TABLE users ADD COLUMN IF NOT EXISTS phone_number VARCHAR ( 20 ); +``` + +against `config.user.tables?.users?.name` (default `users`). It runs on `onReady`, not at registration time, because the table is created while `@prefabs.tech/fastify-user` registers — and this plugin has to be registered *before* that one. + +It also augments the `User` interface from `@prefabs.tech/fastify-user` with `phoneNumber?: string`, so the field is typed wherever `User`, `UserCreateInput`, or `request.user` is used in an app that registers this plugin. + +## GraphQL + +`@prefabs.tech/fastify-user` does not carry `phoneNumber` in its `User` SDL, so this plugin adds it at runtime. In the same `onReady` hook as the migration it calls: + +```typescript +fastify.graphql.extendSchema(` + extend type User { + phoneNumber: String + } +`); +``` + +No consumer wiring is required — merge `userSchema` as you normally would and the field appears on the `User` type. + +Details: + +- **No resolver is needed.** The default field resolver reads `phoneNumber` off the row, which the slonik interceptor camelizes from `phone_number`; the user service selects `users.*`, so the value is already there. +- **It is skipped, not failed, when there is nothing to extend.** The hook checks `fastify.graphql?.schema?.getType("User")` first, so an app with `config.graphql.enabled = false` — or one that never merged `userSchema` — boots normally. Without that guard `extendSchema` throws `Cannot extend type "User" because it is not defined.` +- **Registration order does not matter.** The call happens on `onReady`, by which point mercurius has been registered regardless of whether this plugin was registered before or after `@prefabs.tech/fastify-graphql`. +- The REST response schema is separate and unaffected — `phoneNumber` is not serialized on the users REST routes. + +## Configuration reference + +`config.phoneAuth`: + +| Key | Type | Default | Notes | +| --- | --- | --- | --- | +| `enabled` | `boolean` | `true` | Only `false` disables; `undefined` means enabled. | +| `contactMethod` | `"EMAIL" \| "EMAIL_OR_PHONE" \| "PHONE"` | `"PHONE"` | | +| `flowType` | `"USER_INPUT_CODE"` | `"USER_INPUT_CODE"` | | +| `fallbackEmailDomain` | `string` | app name + `.com` | Domain of the synthetic email. | +| `enableDevMode` | `boolean` | `false` | Skips Twilio for every number. | +| `devModeOtp` | `string` | — | Required when `enableDevMode` is `true`. | +| `bypassSmsFor` | `string[]` | `[]` | Phone numbers that skip Twilio and accept `devModeOtp`. | +| `twilio` | `TwilioConfig` | — | Required unless `enableDevMode` is `true`. | +| `override` | `{ apis?, functions? }` | — | Per-API/per-function wrappers, applied after the built-in overrides. | +| `recipe` | `(fastify) => TypeInput` | — | Full escape hatch: replaces the generated recipe config entirely. | + +`TwilioConfig` is SuperTokens' `TwilioServiceConfig` without `from` and `messagingServiceSid`, plus a required `verifyServiceSid`. + +### Disabling the plugin + +```typescript +phoneAuth: { + enabled: false; +} +``` + +No recipe is contributed and the SuperTokens passwordless endpoints are not served. + +### Development without Twilio + +```typescript +phoneAuth: { + devModeOtp: "123456", + enableDevMode: true, + fallbackEmailDomain: "example.com", +} +``` + +Every number accepts `123456` and no SMS is sent. To keep Twilio live for real users but bypass it for a handful of test numbers, leave `enableDevMode` off and use `bypassSmsFor` together with `devModeOtp`. + +## Overriding behaviour + +Wrappers receive the original implementation and the Fastify instance, and are applied **after** the built-in overrides — so replacing `consumeCodePOST` or `consumeCode` removes the Twilio Verify integration or the local user creation respectively. + +```typescript +phoneAuth: { + override: { + apis: { + consumeCodePOST: (originalImplementation, fastify) => async (input) => { + fastify.log.info("consuming a passwordless code"); + + return originalImplementation.consumeCodePOST!(input); + }, + }, + }, +} +``` + +For total control, bypass the generated config entirely: + +```typescript +phoneAuth: { + recipe: (fastify) => ({ + contactMethod: "PHONE", + flowType: "USER_INPUT_CODE", + }), +} +``` + +## Validation and failure modes + +`getPasswordlessRecipeConfig` runs during `supertokens.init()`, so configuration mistakes fail at boot rather than on the first sign-in attempt: + +- No `config.phoneAuth` at all → `Phone auth config is missing.` +- `enableDevMode: true` without `devModeOtp` → `phoneAuth.devModeOtp is required when phoneAuth.enableDevMode is true` +- Not in dev mode and `twilio` missing or incomplete → `Twilio config is missing for phone auth.` / `accountSid and ... authToken are required` + +At request time, a Twilio Verify failure is logged and returned as `RESTART_FLOW_ERROR`; a rejected code returns `INCORRECT_USER_INPUT_CODE_ERROR`. + +## Known limitation + +`userContext.phoneNumber` is only set by the `createCodePOST` override, so it is unset on the **resend** path (`resendCodePOST` is not overridden). The `bypassSmsFor` check inside `getCustomUserInputCode` therefore cannot match on a resend. diff --git a/packages/phone-auth/README.md b/packages/phone-auth/README.md new file mode 100644 index 000000000..afa72193f --- /dev/null +++ b/packages/phone-auth/README.md @@ -0,0 +1,96 @@ +# @prefabs.tech/fastify-phone-auth + +A [Fastify](https://github.com/fastify/fastify) plugin that adds phone/SMS OTP passwordless login to an API built on [@prefabs.tech/fastify-user](../user/), backed by the [Twilio Verify](https://www.twilio.com/docs/verify) API. + +## Why this plugin? + +SuperTokens ships a Passwordless recipe, but wiring it to Twilio Verify and to your own `users` table is a surprising amount of work — SuperTokens wants to own the OTP, Twilio Verify wants to own the OTP, and neither knows about your database. This plugin exists to: + +- **Bridge SuperTokens and Twilio Verify**: Twilio Verify generates, delivers and checks the real OTP; SuperTokens is handed a placeholder code so its own flow still completes. All of that is hidden behind one plugin registration. +- **Keep the auth package lean**: passwordless is opt-in. Apps that do not use it never install `twilio`, and `@prefabs.tech/fastify-user` carries no passwordless config surface. +- **Initialise the recipe automatically**: registering this plugin is all it takes — the SuperTokens Passwordless recipe is contributed to `@prefabs.tech/fastify-user`'s recipe list for you. +- **Create the local user row**: on first sign-in a matching row is created in your `users` table with the phone number and a synthetic `@` email, since SuperTokens requires an email. +- **Own the `phone_number` column**: an idempotent migration adds it to the users table, the `User` type from `@prefabs.tech/fastify-user` is augmented with `phoneNumber?: string`, and the GraphQL `User` type is extended at runtime — no wiring on your side. +- **Support local development without Twilio**: a dev mode and a per-number bypass list accept a fixed OTP so you can develop and test without sending real SMS. + +## Requirements + +Peer dependencies (install compatible versions — see [package.json](./package.json)): + +- [@prefabs.tech/fastify-config](../config/) +- [@prefabs.tech/fastify-error-handler](../error-handler/) +- [@prefabs.tech/fastify-slonik](../slonik/) +- [@prefabs.tech/fastify-user](../user/) +- [`fastify`](https://www.npmjs.com/package/fastify) +- [`fastify-plugin`](https://www.npmjs.com/package/fastify-plugin) +- [`slonik`](https://www.npmjs.com/package/slonik) +- [`supertokens-node`](https://www.npmjs.com/package/supertokens-node) + +## Installation + +Install with npm: + +```bash +npm install @prefabs.tech/fastify-config @prefabs.tech/fastify-error-handler @prefabs.tech/fastify-slonik @prefabs.tech/fastify-user @prefabs.tech/fastify-phone-auth fastify fastify-plugin slonik supertokens-node +``` + +Install with pnpm: + +```bash +pnpm add --filter "@scope/project" @prefabs.tech/fastify-config @prefabs.tech/fastify-error-handler @prefabs.tech/fastify-slonik @prefabs.tech/fastify-user @prefabs.tech/fastify-phone-auth fastify fastify-plugin slonik supertokens-node +``` + +## Usage + +### Register the plugin — before `@prefabs.tech/fastify-user` + +SuperTokens allows exactly one global `init()`, and `@prefabs.tech/fastify-user` performs it while it is being registered. This plugin therefore has to be registered **first**, so its recipe is in the list by the time that happens. + +```typescript +import configPlugin from "@prefabs.tech/fastify-config"; +import phoneAuthPlugin from "@prefabs.tech/fastify-phone-auth"; +import slonikPlugin from "@prefabs.tech/fastify-slonik"; +import userPlugin from "@prefabs.tech/fastify-user"; +import Fastify from "fastify"; + +const fastify = Fastify(); + +await fastify.register(configPlugin, { config }); +await fastify.register(slonikPlugin); +await fastify.register(phoneAuthPlugin); // contributes the recipe +await fastify.register(userPlugin); // runs supertokens.init() +``` + +Registering it after `@prefabs.tech/fastify-user` throws: + +``` +SuperTokens is already initialised. Register SuperTokens recipe plugins before @prefabs.tech/fastify-user. +``` + +### Configuration + +```typescript +const config: ApiConfig = { + // ... + phoneAuth: { + fallbackEmailDomain: "example.com", + twilio: { + accountSid: process.env.TWILIO_ACCOUNT_SID, + authToken: process.env.TWILIO_AUTH_TOKEN, + verifyServiceSid: process.env.TWILIO_VERIFY_SERVICE_SID, + }, + }, +}; +``` + +For local development, skip Twilio entirely: + +```typescript +phoneAuth: { + devModeOtp: "123456", + enableDevMode: true, + fallbackEmailDomain: "example.com", +} +``` + +See the [developer guide](./GUIDE.md) for the full configuration reference, the SuperTokens endpoints this exposes, and the override hooks. diff --git a/packages/phone-auth/eslint.config.js b/packages/phone-auth/eslint.config.js new file mode 100644 index 000000000..d95745548 --- /dev/null +++ b/packages/phone-auth/eslint.config.js @@ -0,0 +1,11 @@ +import fastifyConfig from "@prefabs.tech/eslint-config/fastify.js"; + +export default [ + ...fastifyConfig, + { + files: ["**/__test__/**"], + rules: { + "unicorn/filename-case": "off", + }, + }, +]; diff --git a/packages/phone-auth/package.json b/packages/phone-auth/package.json new file mode 100644 index 000000000..9fb6a487e --- /dev/null +++ b/packages/phone-auth/package.json @@ -0,0 +1,68 @@ +{ + "name": "@prefabs.tech/fastify-phone-auth", + "version": "0.94.1", + "description": "Fastify phone auth plugin", + "homepage": "https://github.com/prefabs-tech/fastify/tree/main/packages/phone-auth#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/prefabs-tech/fastify.git", + "directory": "packages/phone-auth" + }, + "license": "MIT", + "type": "module", + "exports": { + ".": { + "import": "./dist/prefabs-tech-fastify-phone-auth.js", + "require": "./dist/prefabs-tech-fastify-phone-auth.cjs" + } + }, + "main": "./dist/prefabs-tech-fastify-phone-auth.cjs", + "module": "./dist/prefabs-tech-fastify-phone-auth.js", + "types": "./dist/types/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "vite build && tsc --emitDeclarationOnly && mv dist/src dist/types", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "sort-package": "npx sort-package-json", + "test": "vitest run --coverage", + "typecheck": "tsc --noEmit -p tsconfig.json --composite false" + }, + "dependencies": { + "twilio": "6.0.0" + }, + "devDependencies": { + "@prefabs.tech/eslint-config": "0.8.7", + "@prefabs.tech/fastify-config": "0.94.1", + "@prefabs.tech/fastify-error-handler": "0.94.1", + "@prefabs.tech/fastify-slonik": "0.94.1", + "@prefabs.tech/fastify-user": "0.94.1", + "@prefabs.tech/tsconfig": "0.8.7", + "@types/node": "24.13.3", + "@vitest/coverage-istanbul": "3.2.7", + "eslint": "10.7.0", + "fastify": "5.10.0", + "fastify-plugin": "6.0.0", + "prettier": "3.9.5", + "slonik": "46.8.0", + "supertokens-node": "14.1.4", + "typescript": "5.9.3", + "vite": "8.1.5", + "vitest": "3.2.7" + }, + "peerDependencies": { + "@prefabs.tech/fastify-config": "0.94.1", + "@prefabs.tech/fastify-error-handler": "0.94.1", + "@prefabs.tech/fastify-slonik": "0.94.1", + "@prefabs.tech/fastify-user": "0.94.1", + "fastify": ">=5.10.0", + "fastify-plugin": ">=5.1.0", + "slonik": ">=46.8.0", + "supertokens-node": ">=14.1.4" + }, + "engines": { + "node": ">=20" + } +} diff --git a/packages/phone-auth/src/__test__/extendUserSchema.test.ts b/packages/phone-auth/src/__test__/extendUserSchema.test.ts new file mode 100644 index 000000000..2fb41c43d --- /dev/null +++ b/packages/phone-auth/src/__test__/extendUserSchema.test.ts @@ -0,0 +1,77 @@ +import type { FastifyInstance } from "fastify"; + +/* istanbul ignore file */ +import Fastify from "fastify"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import plugin from "../plugin"; + +vi.mock("../migrations/runMigrations", () => ({ + default: vi.fn(), +})); + +// Stands in for the decorator mercurius adds; mercurius itself is not a +// dependency of this package. +const buildGraphqlDecorator = (userTypeExists: boolean) => ({ + extendSchema: vi.fn(), + schema: { getType: vi.fn(() => (userTypeExists ? {} : undefined)) }, +}); + +const buildFastify = ( + graphql?: ReturnType, +): FastifyInstance => { + const fastify = Fastify({ logger: false }); + + fastify.decorate("config", { appName: "Test App", phoneAuth: {} }); + fastify.decorate("slonik", {}); + + if (graphql) { + fastify.decorate( + "graphql", + graphql as unknown as FastifyInstance["graphql"], + ); + } + + return fastify; +}; + +describe("extendUserSchema", () => { + let fastify: FastifyInstance; + + afterEach(async () => { + await fastify.close(); + }); + + it("adds phoneNumber to the User type when the schema defines it", async () => { + const graphql = buildGraphqlDecorator(true); + fastify = buildFastify(graphql); + + await fastify.register(plugin); + await fastify.ready(); + + expect(graphql.schema.getType).toHaveBeenCalledWith("User"); + expect(graphql.extendSchema).toHaveBeenCalledTimes(1); + expect(graphql.extendSchema.mock.calls[0][0]).toContain("extend type User"); + expect(graphql.extendSchema.mock.calls[0][0]).toContain( + "phoneNumber: String", + ); + }); + + it("does not extend the schema when the User type is absent", async () => { + const graphql = buildGraphqlDecorator(false); + fastify = buildFastify(graphql); + + await fastify.register(plugin); + await fastify.ready(); + + expect(graphql.extendSchema).not.toHaveBeenCalled(); + }); + + it("does not extend the schema when mercurius is not registered", async () => { + fastify = buildFastify(); + + await fastify.register(plugin); + + await expect(fastify.ready()).resolves.toBeDefined(); + }); +}); diff --git a/packages/phone-auth/src/__test__/plugin.test.ts b/packages/phone-auth/src/__test__/plugin.test.ts new file mode 100644 index 000000000..e43aac1f6 --- /dev/null +++ b/packages/phone-auth/src/__test__/plugin.test.ts @@ -0,0 +1,100 @@ +import type { FastifyInstance } from "fastify"; + +/* istanbul ignore file */ +import Fastify from "fastify"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import runMigrations from "../migrations/runMigrations"; +import plugin from "../plugin"; + +vi.mock("../migrations/runMigrations", () => ({ + default: vi.fn(), +})); + +/** + * Builds a Fastify instance decorated with everything the phone auth plugin + * reads. `addSupertokensRecipe` comes from @prefabs.tech/fastify-user and only + * touches decorators, so no SuperTokens init happens here. + */ +const buildFastify = ( + phoneAuthConfig?: Record, +): FastifyInstance => { + const fastify = Fastify({ logger: false }); + + fastify.decorate("config", { + appName: "Test App", + phoneAuth: phoneAuthConfig, + }); + + fastify.decorate("slonik", {}); + + return fastify; +}; + +describe("phoneAuthPlugin", () => { + let fastify: FastifyInstance; + + beforeEach(() => { + vi.mocked(runMigrations).mockClear(); + }); + + afterEach(async () => { + await fastify.close(); + }); + + it("registers the recipe factory when enabled is undefined", async () => { + fastify = buildFastify({}); + await fastify.register(plugin); + + expect(fastify.supertokensRecipes).toHaveLength(1); + }); + + it("registers the recipe factory when enabled is true", async () => { + fastify = buildFastify({ enabled: true }); + await fastify.register(plugin); + + expect(fastify.supertokensRecipes).toHaveLength(1); + }); + + it("registers the recipe factory when the phone auth config is absent", async () => { + fastify = buildFastify(); + await fastify.register(plugin); + + expect(fastify.supertokensRecipes).toHaveLength(1); + }); + + it("registers no recipe factory when enabled === false", async () => { + fastify = buildFastify({ enabled: false }); + await fastify.register(plugin); + + expect(fastify.supertokensRecipes).toBeUndefined(); + }); + + it("runs the migration on ready, not during registration", async () => { + fastify = buildFastify({}); + await fastify.register(plugin); + + expect(runMigrations).not.toHaveBeenCalled(); + + await fastify.ready(); + + expect(runMigrations).toHaveBeenCalledWith(fastify.config, fastify.slonik); + }); + + it("runs no migration when enabled === false", async () => { + fastify = buildFastify({ enabled: false }); + await fastify.register(plugin); + await fastify.ready(); + + expect(runMigrations).not.toHaveBeenCalled(); + }); + + it("throws when registered after SuperTokens has already been initialised", async () => { + fastify = buildFastify({}); + fastify.decorate("supertokensInitialized", true); + + await expect(fastify.register(plugin)).rejects.toThrow( + /Register SuperTokens recipe plugins before @prefabs.tech\/fastify-user/, + ); + }); +}); diff --git a/packages/phone-auth/src/__test__/recipeConfig.spec.ts b/packages/phone-auth/src/__test__/recipeConfig.spec.ts new file mode 100644 index 000000000..c191e14c8 --- /dev/null +++ b/packages/phone-auth/src/__test__/recipeConfig.spec.ts @@ -0,0 +1,137 @@ +import type { FastifyInstance } from "fastify"; + +/* istanbul ignore file */ +import Fastify from "fastify"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { + DEFAULT_CONTACT_METHOD, + DEFAULT_FLOW_TYPE, + TWILIO_VERIFY_PLACEHOLDER_CODE, +} from "../constants"; + +const twilioClientMock = { + client: { verify: { v2: { services: vi.fn() } } }, + verifyServiceSid: "VA123", +}; + +// getTwilioClient is our own module and is the only thing here that reaches an +// external service, so it is the mock seam. +vi.mock("../lib/getTwilioClient", () => ({ + default: vi.fn(() => twilioClientMock), +})); + +const { default: getPasswordlessRecipeConfig } = + await import("../recipe/config"); + +const twilio = { + accountSid: "AC123", + authToken: "token", + verifyServiceSid: "VA123", +}; + +const buildFastify = ( + phoneAuthConfig?: Record, +): FastifyInstance => { + const fastify = Fastify({ logger: false }); + + fastify.decorate("config", { + appName: "Test App", + phoneAuth: phoneAuthConfig, + }); + + return fastify; +}; + +describe("getPasswordlessRecipeConfig", () => { + let fastify: FastifyInstance; + + afterEach(async () => { + await fastify.close(); + }); + + it("defaults contactMethod and flowType", () => { + fastify = buildFastify({ twilio }); + + const config = getPasswordlessRecipeConfig(fastify); + + expect(config.contactMethod).toBe(DEFAULT_CONTACT_METHOD); + expect(config.flowType).toBe(DEFAULT_FLOW_TYPE); + }); + + it("honours a configured contactMethod", () => { + fastify = buildFastify({ contactMethod: "EMAIL_OR_PHONE", twilio }); + + expect(getPasswordlessRecipeConfig(fastify).contactMethod).toBe( + "EMAIL_OR_PHONE", + ); + }); + + it("throws when the phone auth config is missing", () => { + fastify = buildFastify(); + + expect(() => getPasswordlessRecipeConfig(fastify)).toThrow( + /Phone auth config is missing/, + ); + }); + + it("throws when dev mode is on without a devModeOtp", () => { + fastify = buildFastify({ enableDevMode: true }); + + expect(() => getPasswordlessRecipeConfig(fastify)).toThrow( + /devModeOtp is required/, + ); + }); + + it("returns the dev mode OTP for every number in dev mode", async () => { + fastify = buildFastify({ devModeOtp: "123456", enableDevMode: true }); + + const { getCustomUserInputCode } = getPasswordlessRecipeConfig(fastify); + + await expect( + getCustomUserInputCode!({ phoneNumber: "+15550001111" }), + ).resolves.toBe("123456"); + }); + + it("returns the dev mode OTP for a bypassed number outside dev mode", async () => { + fastify = buildFastify({ + bypassSmsFor: ["+15550001111"], + devModeOtp: "123456", + twilio, + }); + + const { getCustomUserInputCode } = getPasswordlessRecipeConfig(fastify); + + await expect( + getCustomUserInputCode!({ phoneNumber: "+15550001111" }), + ).resolves.toBe("123456"); + }); + + it("returns the Twilio Verify placeholder for a regular number", async () => { + fastify = buildFastify({ bypassSmsFor: ["+15550001111"], twilio }); + + const { getCustomUserInputCode } = getPasswordlessRecipeConfig(fastify); + + await expect( + getCustomUserInputCode!({ phoneNumber: "+15559998888" }), + ).resolves.toBe(TWILIO_VERIFY_PLACEHOLDER_CODE); + }); + + it("swaps SMS delivery for a log line in dev mode", () => { + fastify = buildFastify({ devModeOtp: "123456", enableDevMode: true }); + + const config = getPasswordlessRecipeConfig(fastify); + + expect(config.createAndSendCustomTextMessage).toBeDefined(); + expect(config.smsDelivery).toBeUndefined(); + }); + + it("uses Twilio SMS delivery outside dev mode", () => { + fastify = buildFastify({ twilio }); + + const config = getPasswordlessRecipeConfig(fastify); + + expect(config.smsDelivery).toBeDefined(); + expect(config.createAndSendCustomTextMessage).toBeUndefined(); + }); +}); diff --git a/packages/phone-auth/src/constants.ts b/packages/phone-auth/src/constants.ts new file mode 100644 index 000000000..5bbf070e6 --- /dev/null +++ b/packages/phone-auth/src/constants.ts @@ -0,0 +1,18 @@ +const DEFAULT_CONTACT_METHOD = "PHONE"; +const DEFAULT_FLOW_TYPE = "USER_INPUT_CODE"; + +const ERROR_CODES = { + SIGNUP_FAILED_ERROR: "SIGNUP_FAILED_ERROR", +}; + +// SuperTokens insists on storing a user input code of its own. When Twilio +// Verify owns the real OTP we hand SuperTokens this placeholder instead, and +// replay it once Twilio has approved the code the user actually typed. +const TWILIO_VERIFY_PLACEHOLDER_CODE = "000000"; + +export { + DEFAULT_CONTACT_METHOD, + DEFAULT_FLOW_TYPE, + ERROR_CODES, + TWILIO_VERIFY_PLACEHOLDER_CODE, +}; diff --git a/packages/phone-auth/src/graphql/extendUserSchema.ts b/packages/phone-auth/src/graphql/extendUserSchema.ts new file mode 100644 index 000000000..88c2c5acc --- /dev/null +++ b/packages/phone-auth/src/graphql/extendUserSchema.ts @@ -0,0 +1,21 @@ +import type { FastifyInstance } from "fastify"; + +const USER_SCHEMA_EXTENSION = ` + extend type User { + phoneNumber: String + } +`; + +const extendUserSchema = async (fastify: FastifyInstance) => { + // fastify.graphql exists only once mercurius is registered, and the extension + // needs the User type that @prefabs.tech/fastify-user contributes. Extending + // a type that is not defined throws, so an app running without GraphQL — or + // without the user schema merged — must be left alone. + if (!fastify.graphql?.schema?.getType("User")) { + return; + } + + await fastify.graphql.extendSchema(USER_SCHEMA_EXTENSION); +}; + +export default extendUserSchema; diff --git a/packages/phone-auth/src/index.ts b/packages/phone-auth/src/index.ts new file mode 100644 index 000000000..c82c3ce62 --- /dev/null +++ b/packages/phone-auth/src/index.ts @@ -0,0 +1,24 @@ +import type { PhoneAuthConfig } from "./types"; + +declare module "@prefabs.tech/fastify-config" { + interface ApiConfig { + phoneAuth?: PhoneAuthConfig; + } +} + +declare module "@prefabs.tech/fastify-user" { + interface User { + phoneNumber?: string; + } +} + +export * from "./constants"; + +export { default as getTwilioClient } from "./lib/getTwilioClient"; +export { default } from "./plugin"; +export { default as getPasswordlessRecipeConfig } from "./recipe/config"; +export { default as consumeCode } from "./recipe/consumeCode"; +export { default as consumeCodePOST } from "./recipe/consumeCodePost"; +export { default as initPasswordlessRecipe } from "./recipe/initPasswordlessRecipe"; + +export type * from "./types"; diff --git a/packages/phone-auth/src/lib/getTwilioClient.ts b/packages/phone-auth/src/lib/getTwilioClient.ts new file mode 100644 index 000000000..ca3db3fc2 --- /dev/null +++ b/packages/phone-auth/src/lib/getTwilioClient.ts @@ -0,0 +1,30 @@ +import twilio from "twilio"; + +import type { TwilioConfig } from "../types"; + +const getTwilioClient = (config: TwilioConfig | undefined) => { + if (!config) { + throw new Error( + "Twilio config is missing for phone auth. Add `phoneAuth.twilio` to your app config.", + ); + } + + if (!config.verifyServiceSid) { + throw new Error( + "phoneAuth.twilio.verifyServiceSid is required for phone auth verification", + ); + } + + if (!config.accountSid || !config.authToken) { + throw new Error( + "phoneAuth.twilio.accountSid and phoneAuth.twilio.authToken are required for phone auth verification", + ); + } + + return { + client: twilio(config.accountSid, config.authToken), + verifyServiceSid: config.verifyServiceSid, + }; +}; + +export default getTwilioClient; diff --git a/packages/phone-auth/src/migrations/queries.ts b/packages/phone-auth/src/migrations/queries.ts new file mode 100644 index 000000000..b87ba851f --- /dev/null +++ b/packages/phone-auth/src/migrations/queries.ts @@ -0,0 +1,16 @@ +import type { ApiConfig } from "@prefabs.tech/fastify-config"; +import type { QuerySqlToken } from "slonik"; + +import { TABLE_USERS } from "@prefabs.tech/fastify-user"; +import { sql } from "slonik"; + +const addPhoneNumberInUsersTableQuery = (config: ApiConfig): QuerySqlToken => { + const users = config.user.tables?.users?.name || TABLE_USERS; + + return sql.unsafe` + ALTER TABLE ${sql.identifier([users])} + ADD COLUMN IF NOT EXISTS phone_number VARCHAR ( 20 ); + `; +}; + +export { addPhoneNumberInUsersTableQuery }; diff --git a/packages/phone-auth/src/migrations/runMigrations.ts b/packages/phone-auth/src/migrations/runMigrations.ts new file mode 100644 index 000000000..ab813dd8c --- /dev/null +++ b/packages/phone-auth/src/migrations/runMigrations.ts @@ -0,0 +1,12 @@ +import type { ApiConfig } from "@prefabs.tech/fastify-config"; +import type { Database } from "@prefabs.tech/fastify-slonik"; + +import { addPhoneNumberInUsersTableQuery } from "./queries"; + +const runMigrations = async (config: ApiConfig, database: Database) => { + await database.connect(async (connection) => { + await connection.query(addPhoneNumberInUsersTableQuery(config)); + }); +}; + +export default runMigrations; diff --git a/packages/phone-auth/src/plugin.ts b/packages/phone-auth/src/plugin.ts new file mode 100644 index 000000000..8cd7bc943 --- /dev/null +++ b/packages/phone-auth/src/plugin.ts @@ -0,0 +1,31 @@ +import type { FastifyPluginAsync } from "fastify"; + +import { addSupertokensRecipe } from "@prefabs.tech/fastify-user"; +import FastifyPlugin from "fastify-plugin"; + +import extendUserSchema from "./graphql/extendUserSchema"; +import runMigrations from "./migrations/runMigrations"; +import initPasswordlessRecipe from "./recipe/initPasswordlessRecipe"; + +const phoneAuthPlugin: FastifyPluginAsync = async (fastify) => { + if (fastify.config.phoneAuth?.enabled === false) { + fastify.log.info("fastify-phone-auth plugin is not enabled"); + + return; + } + + fastify.log.info("Registering fastify-phone-auth plugin"); + + addSupertokensRecipe(fastify, initPasswordlessRecipe); + + // The migration alters the users table, which @prefabs.tech/fastify-user + // creates during its own registration — and this plugin has to be registered + // BEFORE that one (see addSupertokensRecipe). onReady is the only point where + // the table is guaranteed to exist. + fastify.addHook("onReady", async () => { + await runMigrations(fastify.config, fastify.slonik); + await extendUserSchema(fastify); + }); +}; + +export default FastifyPlugin(phoneAuthPlugin); diff --git a/packages/phone-auth/src/recipe/config.ts b/packages/phone-auth/src/recipe/config.ts new file mode 100644 index 000000000..f231fd426 --- /dev/null +++ b/packages/phone-auth/src/recipe/config.ts @@ -0,0 +1,186 @@ +import type { FastifyInstance } from "fastify"; +import type { + APIInterface, + TypeInput as PasswordlessRecipeConfig, + RecipeInterface, +} from "supertokens-node/recipe/passwordless/types"; + +import type { PhoneAuthConfig } from "../types"; + +import { + DEFAULT_CONTACT_METHOD, + DEFAULT_FLOW_TYPE, + TWILIO_VERIFY_PLACEHOLDER_CODE, +} from "../constants"; +import getTwilioClient from "../lib/getTwilioClient"; +import consumeCode from "./consumeCode"; +import consumeCodePOST from "./consumeCodePost"; + +// SuperTokens has no first-class support for the Twilio Verify API, so both +// consumeCodePOST and consumeCode are overridden to bridge the two. +// +// How it works: +// 1. Sign in/up hits createCodePOST, which asks Twilio Verify to send an OTP to +// the phone number. +// 2. SuperTokens still requires a user input code of its own, so it stores +// TWILIO_VERIFY_PLACEHOLDER_CODE instead of the real OTP. +// 3. On consumeCodePOST the submitted OTP is checked against Twilio Verify. If +// Twilio approves, the original consumeCodePOST is replayed with +// TWILIO_VERIFY_PLACEHOLDER_CODE so SuperTokens can complete its own flow. +// 4. consumeCode then creates the matching row in our database, with a +// synthetic `@` email because SuperTokens +// requires an email field. + +const getPasswordlessRecipeConfig = ( + fastify: FastifyInstance, +): PasswordlessRecipeConfig => { + const phoneAuth: PhoneAuthConfig | undefined = fastify.config.phoneAuth; + + if (!phoneAuth) { + throw new Error( + "Phone auth config is missing. Add `phoneAuth` to your app config.", + ); + } + + const isDevelopment = phoneAuth.enableDevMode === true; + const developmentModeOtp = phoneAuth.devModeOtp; + + if (isDevelopment && !developmentModeOtp) { + throw new Error( + "phoneAuth.devModeOtp is required when phoneAuth.enableDevMode is true", + ); + } + + const isDevelopmentNumber = (phoneNumber: string) => { + return (phoneAuth.bypassSmsFor || []).includes(phoneNumber); + }; + + // Fail at boot rather than on the first sign-in attempt. + if (!isDevelopment) { + getTwilioClient(phoneAuth.twilio); + } + + return { + contactMethod: phoneAuth.contactMethod || DEFAULT_CONTACT_METHOD, + flowType: phoneAuth.flowType || DEFAULT_FLOW_TYPE, + getCustomUserInputCode: async (userContext) => { + const phoneNumber = userContext?.phoneNumber as string | undefined; + + if (isDevelopment || (phoneNumber && isDevelopmentNumber(phoneNumber))) { + return developmentModeOtp as string; + } + + return TWILIO_VERIFY_PLACEHOLDER_CODE; + }, + override: { + apis: (originalImplementation) => { + const apiInterface: Partial = {}; + + if (phoneAuth.override?.apis) { + const apis = phoneAuth.override.apis; + + let api: keyof APIInterface; + + for (api in apis) { + const apiWrapper = apis[api]; + + if (apiWrapper) { + apiInterface[api] = apiWrapper( + originalImplementation, + fastify, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ) as any; + } + } + } + + return { + ...originalImplementation, + consumeCodePOST: consumeCodePOST(originalImplementation, fastify), + createCodePOST: async (input) => { + if ("phoneNumber" in input) { + input.userContext.phoneNumber = input.phoneNumber; + } + + return originalImplementation.createCodePOST!(input); + }, + ...apiInterface, + }; + }, + functions: (originalImplementation) => { + const recipeInterface: Partial = {}; + + if (phoneAuth.override?.functions) { + const recipes = phoneAuth.override.functions; + + let recipe: keyof RecipeInterface; + + for (recipe in recipes) { + const recipeWrapper = recipes[recipe]; + + if (recipeWrapper) { + recipeInterface[recipe] = recipeWrapper( + originalImplementation, + fastify, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ) as any; + } + } + } + + return { + ...originalImplementation, + consumeCode: consumeCode(originalImplementation, fastify), + ...recipeInterface, + }; + }, + }, + ...(isDevelopment + ? { + createAndSendCustomTextMessage: async () => { + fastify.log.info( + `Skipping phone auth SMS delivery in development environment. Use default OTP [${developmentModeOtp}] for testing.`, + ); + }, + } + : { + smsDelivery: { + override: (originalImplementation) => { + return { + ...originalImplementation, + sendSms: async (input: { phoneNumber: string }) => { + if (isDevelopmentNumber(input.phoneNumber)) { + fastify.log.info( + `Skipping SMS for test number ${input.phoneNumber}.`, + ); + + return; + } + + const { client, verifyServiceSid } = getTwilioClient( + phoneAuth.twilio, + ); + + try { + await client.verify.v2 + .services(verifyServiceSid) + .verifications.create({ + channel: "sms", + to: input.phoneNumber, + }); + } catch (error) { + fastify.log.error( + error, + "Twilio Verify failed to send OTP", + ); + throw error; + } + }, + }; + }, + }, + }), + }; +}; + +export default getPasswordlessRecipeConfig; diff --git a/packages/phone-auth/src/recipe/consumeCode.ts b/packages/phone-auth/src/recipe/consumeCode.ts new file mode 100644 index 000000000..e6fffdb4e --- /dev/null +++ b/packages/phone-auth/src/recipe/consumeCode.ts @@ -0,0 +1,125 @@ +import type { User } from "@prefabs.tech/fastify-user"; +import type { FastifyInstance, FastifyRequest } from "fastify"; +import type { RecipeInterface } from "supertokens-node/recipe/passwordless/types"; + +import { CustomError } from "@prefabs.tech/fastify-error-handler"; +import { formatDate } from "@prefabs.tech/fastify-slonik"; +import { + areRolesExist, + getUserService, + ROLE_USER, +} from "@prefabs.tech/fastify-user"; +import { deleteUser, getRequestFromUserContext } from "supertokens-node"; +import UserRoles from "supertokens-node/recipe/userroles"; + +import { ERROR_CODES } from "../constants"; + +const consumeCode = ( + originalImplementation: RecipeInterface, + fastify: FastifyInstance, +): RecipeInterface["consumeCode"] => { + return async (input) => { + const roles = (input.userContext.roles || [ + fastify.config.user.role || ROLE_USER, + ]) as string[]; + + if (!(await areRolesExist(roles))) { + throw new CustomError( + `At least one role from ${roles.join(", ")} does not exist.`, + ERROR_CODES.SIGNUP_FAILED_ERROR, + ); + } + + const originalResponse = await originalImplementation.consumeCode(input); + + if (originalResponse.status !== "OK") { + return originalResponse; + } + + const request = getRequestFromUserContext(input.userContext)?.original as + FastifyRequest | undefined; + + const userService = getUserService( + request?.config || fastify.config, + request?.slonik || fastify.slonik, + request?.dbSchema, + ); + + const phoneNumber = originalResponse.user.phoneNumber; + + const emailDomain = + fastify.config.phoneAuth?.fallbackEmailDomain || + fastify.config.appName.toLowerCase().replaceAll(/\s+/g, "") + ".com"; + + const email = phoneNumber + ? `${phoneNumber}@${emailDomain}` + : originalResponse.user.email; + + if (!email || !phoneNumber) { + await deleteUser(originalResponse.user.id); + + throw new Error("Phone auth user missing phone number or email"); + } + + let user: null | undefined | User; + + if (originalResponse.createdNewUser) { + try { + user = await userService.create({ + email, + id: originalResponse.user.id, + phoneNumber, + }); + + if (!user) { + throw new Error("User not found"); + } + } catch (error) { + await deleteUser(originalResponse.user.id); + + throw error; + } + + user.roles = roles; + + originalResponse.user = { + ...originalResponse.user, + ...user, + }; + + for (const role of roles) { + const rolesResponse = await UserRoles.addRoleToUser( + originalResponse.user.id, + role, + ); + + if (rolesResponse.status !== "OK") { + fastify.log.error(rolesResponse.status); + } + } + } else { + await userService + .update(originalResponse.user.id, { + lastLoginAt: formatDate(new Date(Date.now())), + }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .catch((error: any) => { + fastify.log.error( + `Unable to update lastLoginAt for userId ${originalResponse.user.id}`, + ); + fastify.log.error(error); + }); + } + + return { + ...originalResponse, + user: { + ...originalResponse.user, + email, + phoneNumber, + }, + }; + }; +}; + +export default consumeCode; diff --git a/packages/phone-auth/src/recipe/consumeCodePost.ts b/packages/phone-auth/src/recipe/consumeCodePost.ts new file mode 100644 index 000000000..33d480702 --- /dev/null +++ b/packages/phone-auth/src/recipe/consumeCodePost.ts @@ -0,0 +1,115 @@ +import type { FastifyInstance } from "fastify"; +import type { APIInterface } from "supertokens-node/recipe/passwordless/types"; + +import { ROLE_USER } from "@prefabs.tech/fastify-user"; +import Passwordless from "supertokens-node/recipe/passwordless"; + +import { TWILIO_VERIFY_PLACEHOLDER_CODE } from "../constants"; +import getTwilioClient from "../lib/getTwilioClient"; + +const enrichResult = ( + result: Awaited>>, + phoneNumber: string, + fallbackEmailDomain: string, +) => { + if (result.status !== "OK") { + return result; + } + + return { + ...result, + user: { + ...result.user, + email: result.user.email ?? `${phoneNumber}@${fallbackEmailDomain}`, + }, + }; +}; + +const consumeCodePOST = ( + originalImplementation: APIInterface, + fastify: FastifyInstance, +): APIInterface["consumeCodePOST"] => { + return async (input) => { + input.userContext.roles ||= [fastify.config.user.role || ROLE_USER]; + + if (originalImplementation.consumeCodePOST === undefined) { + throw new Error("Should never come here"); + } + + // Only handle user input code flows, not magic link flows + if (!("userInputCode" in input) || input.userInputCode === undefined) { + return originalImplementation.consumeCodePOST(input); + } + + const phoneAuth = fastify.config.phoneAuth; + + if (!phoneAuth) { + throw new Error("Phone auth config is missing"); + } + + const isDevelopment = phoneAuth.enableDevMode === true; + + // Look up the device to retrieve the associated phone number + const deviceContext = await Passwordless.listCodesByPreAuthSessionId({ + preAuthSessionId: input.preAuthSessionId, + }); + + if (!deviceContext || !deviceContext.phoneNumber) { + return { status: "RESTART_FLOW_ERROR" }; + } + + const { phoneNumber } = deviceContext; + const bypassNumbers = phoneAuth.bypassSmsFor ?? []; + const fallbackEmailDomain = phoneAuth.fallbackEmailDomain ?? ""; + + // In dev mode or for bypassed numbers, skip Twilio Verify and let + // SuperTokens verify the code directly (uses devModeOtp) + if (isDevelopment || bypassNumbers.includes(phoneNumber)) { + return enrichResult( + await originalImplementation.consumeCodePOST(input), + phoneNumber, + fallbackEmailDomain, + ); + } + + let client, verifyServiceSid; + + try { + ({ client, verifyServiceSid } = getTwilioClient(phoneAuth.twilio)); + } catch (error) { + fastify.log.error(error); + + return { status: "RESTART_FLOW_ERROR" }; + } + + try { + const check = await client.verify.v2 + .services(verifyServiceSid) + .verificationChecks.create({ + code: input.userInputCode, + to: phoneNumber, + }); + + return check.status === "approved" + ? enrichResult( + await originalImplementation.consumeCodePOST({ + ...input, + userInputCode: TWILIO_VERIFY_PLACEHOLDER_CODE, + }), + phoneNumber, + fallbackEmailDomain, + ) + : { + failedCodeInputAttemptCount: 1, + maximumCodeInputAttempts: 5, + status: "INCORRECT_USER_INPUT_CODE_ERROR", + }; + } catch (error) { + fastify.log.error(error, "Twilio Verify verification check failed"); + + return { status: "RESTART_FLOW_ERROR" }; + } + }; +}; + +export default consumeCodePOST; diff --git a/packages/phone-auth/src/recipe/initPasswordlessRecipe.ts b/packages/phone-auth/src/recipe/initPasswordlessRecipe.ts new file mode 100644 index 000000000..c9f8b9e8d --- /dev/null +++ b/packages/phone-auth/src/recipe/initPasswordlessRecipe.ts @@ -0,0 +1,17 @@ +import type { FastifyInstance } from "fastify"; + +import Passwordless from "supertokens-node/recipe/passwordless"; + +import getPasswordlessRecipeConfig from "./config"; + +const initPasswordlessRecipe = (fastify: FastifyInstance) => { + const recipe = fastify.config.phoneAuth?.recipe; + + if (typeof recipe === "function") { + return Passwordless.init(recipe(fastify)); + } + + return Passwordless.init(getPasswordlessRecipeConfig(fastify)); +}; + +export default initPasswordlessRecipe; diff --git a/packages/phone-auth/src/types.ts b/packages/phone-auth/src/types.ts new file mode 100644 index 000000000..47b8897d0 --- /dev/null +++ b/packages/phone-auth/src/types.ts @@ -0,0 +1,78 @@ +import type { FastifyInstance } from "fastify"; +import type { TwilioServiceConfig } from "supertokens-node/lib/build/ingredients/smsdelivery/services/twilio"; +import type { + APIInterface, + TypeInput as PasswordlessRecipeConfig, + RecipeInterface, +} from "supertokens-node/recipe/passwordless/types"; + +type APIInterfaceWrapper = { + [key in keyof APIInterface]?: ( + originalImplementation: APIInterface, + fastify: FastifyInstance, + ) => APIInterface[key]; +}; + +interface PhoneAuthConfig { + /** + * Phone numbers that skip Twilio entirely and are verified against + * `devModeOtp` instead. + */ + bypassSmsFor?: string[]; + /** + * @default "PHONE" + */ + contactMethod?: "EMAIL" | "EMAIL_OR_PHONE" | "PHONE"; + /** + * Required when `enableDevMode` is true. + */ + devModeOtp?: string; + /** + * @default true + */ + enabled?: boolean; + /** + * Skip Twilio and accept `devModeOtp` for every number. + * @default false + */ + enableDevMode?: boolean; + /** + * SuperTokens requires an email, so passwordless users get a synthetic + * `@` one. Defaults to the app name. + */ + fallbackEmailDomain?: string; + /** + * @default "USER_INPUT_CODE" + */ + flowType?: "USER_INPUT_CODE"; + override?: { + apis?: APIInterfaceWrapper; + functions?: RecipeInterfaceWrapper; + }; + /** + * Full escape hatch: replaces the generated recipe config entirely. + */ + recipe?: (fastify: FastifyInstance) => PasswordlessRecipeConfig; + twilio?: TwilioConfig; +} + +type RecipeInterfaceWrapper = { + [key in keyof RecipeInterface]?: ( + originalImplementation: RecipeInterface, + fastify: FastifyInstance, + ) => RecipeInterface[key]; +}; + +type TwilioConfig = Omit< + TwilioServiceConfig, + "from" | "messagingServiceSid" +> & { + verifyServiceSid: string; +}; + +export type { + APIInterfaceWrapper, + PhoneAuthConfig, + RecipeInterfaceWrapper, + TwilioConfig, +}; diff --git a/packages/phone-auth/tsconfig.json b/packages/phone-auth/tsconfig.json new file mode 100644 index 000000000..1628077b9 --- /dev/null +++ b/packages/phone-auth/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@prefabs.tech/tsconfig/fastify.json", + "exclude": ["src/**/__test__/**/*"], + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist" + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/phone-auth/vite.config.ts b/packages/phone-auth/vite.config.ts new file mode 100644 index 000000000..9a4a5876d --- /dev/null +++ b/packages/phone-auth/vite.config.ts @@ -0,0 +1,60 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { defineConfig, loadEnv } from "vite"; + +import { dependencies, peerDependencies } from "./package.json"; + +// https://vitejs.dev/config/ +export default defineConfig(({ mode }) => { + process.env = { ...process.env, ...loadEnv(mode, process.cwd()) }; + + return { + build: { + lib: { + entry: path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "src/index.ts", + ), + fileName: "prefabs-tech-fastify-phone-auth", + formats: ["cjs", "es"], + name: "PrefabsTechFastifyPhoneAuth", + }, + rolldownOptions: { + external: [ + ...Object.keys(dependencies), + ...Object.keys(peerDependencies), + /supertokens-node+/, + ], + output: { + exports: "named", + globals: { + "@prefabs.tech/fastify-config": "PrefabsTechFastifyConfig", + "@prefabs.tech/fastify-error-handler": + "PrefabsTechFastifyErrorHandler", + "@prefabs.tech/fastify-slonik": "PrefabsTechFastifySlonik", + "@prefabs.tech/fastify-user": "PrefabsTechFastifyUser", + fastify: "Fastify", + "fastify-plugin": "FastifyPlugin", + slonik: "Slonik", + "supertokens-node": "SupertokensNode", + "supertokens-node/recipe/passwordless": "SupertokensPasswordless", + "supertokens-node/recipe/userroles": "SupertokensUserRoles", + twilio: "Twilio", + }, + }, + }, + target: "es2022", + }, + resolve: { + alias: { + "@/": new URL("src/", import.meta.url).pathname, + }, + }, + test: { + coverage: { + provider: "istanbul", + reporter: ["text", "json", "html"], + }, + }, + }; +}); diff --git a/packages/user/FEATURES.md b/packages/user/FEATURES.md index 2d078e63a..0efbe1aa0 100644 --- a/packages/user/FEATURES.md +++ b/packages/user/FEATURES.md @@ -12,80 +12,82 @@ 4. **Default role seeding** — on `onReady`, seeds `ADMIN`, `SUPERADMIN`, and `USER` into SuperTokens, plus any extra roles listed in `config.user.roles`. +5. **SuperTokens recipe registry** — `addSupertokensRecipe(fastify, factory)` lets another plugin contribute a SuperTokens recipe. Factories are collected on the `fastify.supertokensRecipes` decorator and drained by `getRecipeList` during `supertokens.init()`. Since SuperTokens allows exactly one global `init()` and this package performs it during its own registration, contributing plugins must be registered **before** it; `addSupertokensRecipe` throws once `fastify.supertokensInitialized` is set. Used by `@prefabs.tech/fastify-phone-auth`. + ## Authentication -5. **`fastify.verifySession()` decorator** — added to the Fastify instance; use it as a `preHandler` to require a valid SuperTokens session on any route. +6. **`fastify.verifySession()` decorator** — added to the Fastify instance; use it as a `preHandler` to require a valid SuperTokens session on any route. -6. **`req.session` request property** — `FastifyRequest` is augmented with an optional `session` property (populated by SuperTokens after `verifySession` runs). +7. **`req.session` request property** — `FastifyRequest` is augmented with an optional `session` property (populated by SuperTokens after `verifySession` runs). -7. **`req.user` request property** — `FastifyRequest` is augmented with an optional `user: User` property, populated from the database on every verified session. +8. **`req.user` request property** — `FastifyRequest` is augmented with an optional `user: User` property, populated from the database on every verified session. -8. **Configurable refresh-token cookie path** — an `onSend` hook rewrites the `Path` attribute of the `sRefreshToken` cookie to the value of `config.user.supertokens.refreshTokenCookiePath`, so the refresh token is scoped to the refresh endpoint. +9. **Configurable refresh-token cookie path** — an `onSend` hook rewrites the `Path` attribute of the `sRefreshToken` cookie to the value of `config.user.supertokens.refreshTokenCookiePath`, so the refresh token is scoped to the refresh endpoint. -9. **`SUPERTOKENS_CORS_HEADERS` constant** — exports the eight SuperTokens-specific request headers that must be included in `allowedHeaders` when registering `@fastify/cors`: +10. **`SUPERTOKENS_CORS_HEADERS` constant** — exports the eight SuperTokens-specific request headers that must be included in `allowedHeaders` when registering `@fastify/cors`: ``` anti-csrf, authorization, fdi-version, front-token, rid, st-access-token, st-auth-mode, st-refresh-token ``` -10. **SuperTokens error handler auto-registration** — automatically calls `fastify.setErrorHandler(supertokensErrorHandler)` unless `config.user.supertokens.setErrorHandler === false`. +11. **SuperTokens error handler auto-registration** — automatically calls `fastify.setErrorHandler(supertokensErrorHandler)` unless `config.user.supertokens.setErrorHandler === false`. -11. **`supertokensErrorHandler` export** — exported for manual wiring when auto-registration is disabled. +12. **`supertokensErrorHandler` export** — exported for manual wiring when auto-registration is disabled. -12. **Session recipe override via function factory** — each SuperTokens recipe (`session`, `thirdPartyEmailPassword`, `userRoles`, `emailVerification`) can be overridden by supplying a function `(fastify) => RecipeConfig` under `config.user.supertokens.recipes`. The function receives the Fastify instance, enabling access to config and decorators. Providing an object instead of a function merges the object into the default config. +13. **Session recipe override via function factory** — each SuperTokens recipe (`session`, `thirdPartyEmailPassword`, `userRoles`, `emailVerification`) can be overridden by supplying a function `(fastify) => RecipeConfig` under `config.user.supertokens.recipes`. The function receives the Fastify instance, enabling access to config and decorators. Providing an object instead of a function merges the object into the default config. -13. **Override merging for `apis` and `functions`** — when a recipe override includes `override.apis` or `override.functions`, each key is called as `fn(originalImplementation, fastify)` and merged on top of the default implementation, so only the keys you provide are replaced. +14. **Override merging for `apis` and `functions`** — when a recipe override includes `override.apis` or `override.functions`, each key is called as `fn(originalImplementation, fastify)` and merged on top of the default implementation, so only the keys you provide are replaced. -14. **Email verification (opt-in)** — setting `config.user.features.signUp.emailVerification = true` adds the `EmailVerification` recipe and enforces the email-verified claim on protected routes. Default: `false`. +15. **Email verification (opt-in)** — setting `config.user.features.signUp.emailVerification = true` adds the `EmailVerification` recipe and enforces the email-verified claim on protected routes. Default: `false`. -15. **Third-party OAuth providers** — Apple, Facebook, GitHub, and Google providers are configurable via `config.user.supertokens.providers`; custom providers are supported via `providers.custom`. +16. **Third-party OAuth providers** — Apple, Facebook, GitHub, and Google providers are configurable via `config.user.supertokens.providers`; custom providers are supported via `providers.custom`. ## User Management -16. **`GET /me`** — returns the authenticated user's profile. If a photo exists, the `photo.url` field is a pre-signed S3 URL. Session claims (email verification, profile validation) are bypassed so users can always read their own data. +17. **`GET /me`** — returns the authenticated user's profile. If a photo exists, the `photo.url` field is a pre-signed S3 URL. Session claims (email verification, profile validation) are bypassed so users can always read their own data. -17. **`PUT /me`** — updates mutable fields on the current user's profile. Session claims are bypassed. +18. **`PUT /me`** — updates mutable fields on the current user's profile. Session claims are bypassed. -18. **`POST /change-email`** — updates the authenticated user's email address. Gated by `config.user.features.updateEmail.enabled`. Session email-verification claims are bypassed on this route. +19. **`POST /change-email`** — updates the authenticated user's email address. Gated by `config.user.features.updateEmail.enabled`. Session email-verification claims are bypassed on this route. -19. **`POST /change_password`** — validates the current password before updating. Requires a valid session. +20. **`POST /change_password`** — validates the current password before updating. Requires a valid session. -20. **`DELETE /me` with atomic session revocation** — soft-deletes the user record (`deleted_at`) and immediately revokes all active SuperTokens sessions in the same operation. Requires password confirmation. +21. **`DELETE /me` with atomic session revocation** — soft-deletes the user record (`deleted_at`) and immediately revokes all active SuperTokens sessions in the same operation. Requires password confirmation. -21. **`PUT /me/photo`** — accepts `multipart/form-data`, validates MIME type (`image/jpeg`, `image/png`, `image/webp`) and file size, uploads to `{userId}/photo` in the configured S3 bucket, and links the file record to the user. Session claims bypassed. +22. **`PUT /me/photo`** — accepts `multipart/form-data`, validates MIME type (`image/jpeg`, `image/png`, `image/webp`) and file size, uploads to `{userId}/photo` in the configured S3 bucket, and links the file record to the user. Session claims bypassed. -22. **`DELETE /me/photo`** — deletes the photo from S3 and unlinks it from the user record. Session claims bypassed. +23. **`DELETE /me/photo`** — deletes the photo from S3 and unlinks it from the user record. Session claims bypassed. -23. **Configurable photo size limit** — `config.user.photoMaxSizeInMB` (default: `5`). +24. **Configurable photo size limit** — `config.user.photoMaxSizeInMB` (default: `5`). -24. **`POST /signup/admin`** — public endpoint to create the first administrator account without an invitation. +25. **`POST /signup/admin`** — public endpoint to create the first administrator account without an invitation. -25. **`GET /signup/admin`** — public endpoint returning `{ signUp: boolean }` indicating whether admin sign-up is currently available. +26. **`GET /signup/admin`** — public endpoint returning `{ signUp: boolean }` indicating whether admin sign-up is currently available. -26. **`GET /users`** — paginatable list of all users. Requires `users:list` permission. +27. **`GET /users`** — paginatable list of all users. Requires `users:list` permission. -27. **`GET /users/:id`** — fetches a single user by ID. Requires `users:read` permission. +28. **`GET /users/:id`** — fetches a single user by ID. Requires `users:read` permission. -28. **`PUT /users/:id/disable`** — sets the user's `disabled` flag to `true`. Requires `users:disable` permission. +29. **`PUT /users/:id/disable`** — sets the user's `disabled` flag to `true`. Requires `users:disable` permission. -29. **`PUT /users/:id/enable`** — clears the user's `disabled` flag. Requires `users:enable` permission. +30. **`PUT /users/:id/enable`** — clears the user's `disabled` flag. Requires `users:enable` permission. -30. **Immutable field guard (`filterUserUpdateInput`)** — applied automatically before every profile update; silently drops any attempt to set `id`, `email`, `roles`, `lastLoginAt`, `signedUpAt`, `disable`, or `enable`. Handles both camelCase and snake_case variants (e.g. `last_login_at` is also stripped). +31. **Immutable field guard (`filterUserUpdateInput`)** — applied automatically before every profile update; silently drops any attempt to set `id`, `email`, `roles`, `lastLoginAt`, `signedUpAt`, `disable`, or `enable`. Handles both camelCase and snake_case variants (e.g. `last_login_at` is also stripped). -31. **Configurable table names** — `config.user.tables.users.name` and `config.user.tables.invitations.name` override the default table names. +32. **Configurable table names** — `config.user.tables.users.name` and `config.user.tables.invitations.name` override the default table names. -32. **Custom request handlers** — every route handler can be replaced via `config.user.handlers.user.` or `config.user.handlers.invitation.`. +33. **Custom request handlers** — every route handler can be replaced via `config.user.handlers.user.` or `config.user.handlers.invitation.`. ## Authorization -33. **`fastify.hasPermission(permission)` decorator** — added to the Fastify instance; returns a `preHandler` that checks the authenticated user holds the given permission. Returns 401 without a session, 403 without the permission. +34. **`fastify.hasPermission(permission)` decorator** — added to the Fastify instance; returns a `preHandler` that checks the authenticated user holds the given permission. Returns 401 without a session, 403 without the permission. -34. **`hasUserPermission(fastify, userId, permission)` utility** — programmatic permission check; returns a boolean. +35. **`hasUserPermission(fastify, userId, permission)` utility** — programmatic permission check; returns a boolean. -35. **SUPERADMIN bypass** — users with the `SUPERADMIN` role pass all `hasPermission` and `hasUserPermission` checks automatically, without being explicitly granted every permission. +36. **SUPERADMIN bypass** — users with the `SUPERADMIN` role pass all `hasPermission` and `hasUserPermission` checks automatically, without being explicitly granted every permission. -36. **Built-in permission constants** — pre-defined strings to avoid typos: +37. **Built-in permission constants** — pre-defined strings to avoid typos: ``` PERMISSIONS_INVITATIONS_CREATE → "invitations:create" @@ -99,100 +101,100 @@ PERMISSIONS_USERS_READ → "users:read" ``` -37. **Application-defined custom permissions** — `config.user.permissions` registers additional permission strings returned by `GET /permissions`, making them discoverable by role-management UIs. +38. **Application-defined custom permissions** — `config.user.permissions` registers additional permission strings returned by `GET /permissions`, making them discoverable by role-management UIs. ## Roles -38. **Built-in role constants** — `ROLE_ADMIN`, `ROLE_SUPERADMIN`, `ROLE_USER` are exported. +39. **Built-in role constants** — `ROLE_ADMIN`, `ROLE_SUPERADMIN`, `ROLE_USER` are exported. -39. **`POST /roles`** — creates a new role with optional initial permissions. Requires a valid session. +40. **`POST /roles`** — creates a new role with optional initial permissions. Requires a valid session. -40. **`DELETE /roles`** — deletes a role; returns `ROLE_IN_USE` error if any user holds it. Requires a valid session. +41. **`DELETE /roles`** — deletes a role; returns `ROLE_IN_USE` error if any user holds it. Requires a valid session. -41. **`GET /roles`** — returns all roles with their permissions. Requires a valid session. +42. **`GET /roles`** — returns all roles with their permissions. Requires a valid session. -42. **`GET /roles/permissions`** — returns the permissions for a named role. Requires a valid session. +43. **`GET /roles/permissions`** — returns the permissions for a named role. Requires a valid session. -43. **`PUT /roles/permissions`** — replaces the permission set of a named role. Requires a valid session. +44. **`PUT /roles/permissions`** — replaces the permission set of a named role. Requires a valid session. -44. **`isRoleExists(name)` / `areRolesExist(names)` utilities** — programmatic existence checks against SuperTokens. +45. **`isRoleExists(name)` / `areRolesExist(names)` utilities** — programmatic existence checks against SuperTokens. ## Invitations -45. **`POST /invitations`** — creates an invitation record, validates the target email and role, checks for a duplicate pending invitation, and sends the invitation email. Requires `invitations:create` permission. +46. **`POST /invitations`** — creates an invitation record, validates the target email and role, checks for a duplicate pending invitation, and sends the invitation email. Requires `invitations:create` permission. -46. **Configurable invitation expiry** — `config.user.invitation.expireAfterInDays` sets how long an invitation is valid (default: `30`). +47. **Configurable invitation expiry** — `config.user.invitation.expireAfterInDays` sets how long an invitation is valid (default: `30`). -47. **Configurable accept link path** — `config.user.invitation.acceptLinkPath` sets the front-end path embedded in the invitation email (default: `"/signup/token/:token"`). The `:token` placeholder is replaced with the actual token. +48. **Configurable accept link path** — `config.user.invitation.acceptLinkPath` sets the front-end path embedded in the invitation email (default: `"/signup/token/:token"`). The `:token` placeholder is replaced with the actual token. -48. **`GET /invitations/token/:token`** — public endpoint returning the invitation record for UI display before acceptance. +49. **`GET /invitations/token/:token`** — public endpoint returning the invitation record for UI display before acceptance. -49. **`POST /invitations/token/:token`** — public endpoint that validates the invitation, creates a SuperTokens account, opens a session, and optionally calls `config.user.invitation.postAccept(request, invitation, user)`. +50. **`POST /invitations/token/:token`** — public endpoint that validates the invitation, creates a SuperTokens account, opens a session, and optionally calls `config.user.invitation.postAccept(request, invitation, user)`. -50. **`GET /invitations`** — paginatable list of all invitations. Requires `invitations:list` permission. +51. **`GET /invitations`** — paginatable list of all invitations. Requires `invitations:list` permission. -51. **`PUT /invitations/revoke/:id`** — marks an invitation as revoked. Requires `invitations:revoke` permission. +52. **`PUT /invitations/revoke/:id`** — marks an invitation as revoked. Requires `invitations:revoke` permission. -52. **`POST /invitations/resend/:id`** — re-sends the invitation email. Requires `invitations:resend` permission. +53. **`POST /invitations/resend/:id`** — re-sends the invitation email. Requires `invitations:resend` permission. -53. **`DELETE /invitations/:id`** — permanently removes an invitation record. Requires `invitations:delete` permission. +54. **`DELETE /invitations/:id`** — permanently removes an invitation record. Requires `invitations:delete` permission. -54. **`isInvitationValid(invitation)` utility** — returns `true` only when the invitation is pending, non-expired, non-revoked, and non-accepted. +55. **`isInvitationValid(invitation)` utility** — returns `true` only when the invitation is pending, non-expired, non-revoked, and non-accepted. -55. **`computeInvitationExpiresAt(config, explicitDate?)` utility** — computes the expiry timestamp using the configured `expireAfterInDays`, or returns `explicitDate` when provided. +56. **`computeInvitationExpiresAt(config, explicitDate?)` utility** — computes the expiry timestamp using the configured `expireAfterInDays`, or returns `explicitDate` when provided. -56. **`getOrigin(url)` utility** — extracts `scheme://host[:non-default-port]` from a URL string. Returns an empty string for bare hostnames, IP addresses without a scheme, relative paths, or any input that is not a full URL. Default ports (`80` / `443`) are stripped. +57. **`getOrigin(url)` utility** — extracts `scheme://host[:non-default-port]` from a URL string. Returns an empty string for bare hostnames, IP addresses without a scheme, relative paths, or any input that is not a full URL. Default ports (`80` / `443`) are stripped. -57. **`sendInvitation(fastify, invitation, origin)` utility** — sends the invitation email; usable from custom code that bypasses the REST route. +58. **`sendInvitation(fastify, invitation, origin)` utility** — sends the invitation email; usable from custom code that bypasses the REST route. ## Email -58. **`validateEmail(email, config)` utility** — validates an email string against `config.user.email` options using `validator.js`. Returns `{ success: true }` or `{ success: false, message }`. Gracefully falls back to permissive defaults when no email config is provided. +59. **`validateEmail(email, config)` utility** — validates an email string against `config.user.email` options using `validator.js`. Returns `{ success: true }` or `{ success: false, message }`. Gracefully falls back to permissive defaults when no email config is provided. -59. **Email domain whitelist / blacklist** — `config.user.email.host_whitelist` and `config.user.email.host_blacklist` restrict which domains are accepted during sign-up and invitation. +60. **Email domain whitelist / blacklist** — `config.user.email.host_whitelist` and `config.user.email.host_blacklist` restrict which domains are accepted during sign-up and invitation. -60. **Custom email subjects and templates** — `config.user.emailOverrides` overrides the subject and `templateName` for any of the five system emails: `invitation`, `resetPassword`, `resetPasswordNotification`, `emailVerification`, `duplicateEmail`. +61. **Custom email subjects and templates** — `config.user.emailOverrides` overrides the subject and `templateName` for any of the five system emails: `invitation`, `resetPassword`, `resetPasswordNotification`, `emailVerification`, `duplicateEmail`. -61. **`sendEmail(options)` utility** — sends a templated email via `fastify.mailer`; accepts `{ fastify, subject, templateName, to, templateData }`. +62. **`sendEmail(options)` utility** — sends a templated email via `fastify.mailer`; accepts `{ fastify, subject, templateName, to, templateData }`. -62. **`verifyEmail(userId, email)` utility** — programmatically marks a user's email as verified in SuperTokens (useful for invited users who skip the verification link). +63. **`verifyEmail(userId, email)` utility** — programmatically marks a user's email as verified in SuperTokens (useful for invited users who skip the verification link). ## Password -63. **`validatePassword(password, config)` utility** — validates password strength against `config.user.password` options. Returns `{ success: true }` or `{ success: false, message }` listing all failed requirements. +64. **`validatePassword(password, config)` utility** — validates password strength against `config.user.password` options. Returns `{ success: true }` or `{ success: false, message }` listing all failed requirements. -64. **Configurable strength thresholds** — `config.user.password` accepts `minLength` (default: `8`), `minLowercase`, `minUppercase`, `minNumbers`, `minSymbols` (all default to `0` unless configured), and scoring tuning fields (`pointsPerUnique`, `pointsPerRepeat`, `pointsForContaining*`). +65. **Configurable strength thresholds** — `config.user.password` accepts `minLength` (default: `8`), `minLowercase`, `minUppercase`, `minNumbers`, `minSymbols` (all default to `0` unless configured), and scoring tuning fields (`pointsPerUnique`, `pointsPerRepeat`, `pointsForContaining*`). ## Profile Validation Claim -65. **`ProfileValidationClaim` custom session claim** — a SuperTokens `SessionClaim` that checks whether required profile fields are populated. Re-fetched on every request. Enable via `config.user.features.profileValidation.enabled = true` and list required fields in `features.profileValidation.fields`. +66. **`ProfileValidationClaim` custom session claim** — a SuperTokens `SessionClaim` that checks whether required profile fields are populated. Re-fetched on every request. Enable via `config.user.features.profileValidation.enabled = true` and list required fields in `features.profileValidation.fields`. -66. **Grace period** — `config.user.features.profileValidation.gracePeriodInDays` allows users to access protected resources for N days after sign-up before the claim is enforced. After the grace period, requests fail with 403. +67. **Grace period** — `config.user.features.profileValidation.gracePeriodInDays` allows users to access protected resources for N days after sign-up before the claim is enforced. After the grace period, requests fail with 403. -67. **Per-route claim opt-out** — routes that must stay accessible regardless of profile completeness can bypass the claim via `verifySession({ overrideGlobalClaimValidators: () => [] })` (REST) or `@auth(profileValidation: false)` (GraphQL). +68. **Per-route claim opt-out** — routes that must stay accessible regardless of profile completeness can bypass the claim via `verifySession({ overrideGlobalClaimValidators: () => [] })` (REST) or `@auth(profileValidation: false)` (GraphQL). ## GraphQL Integration > Requires `config.graphql.enabled = true` and `@prefabs.tech/fastify-graphql`. -68. **MercuriusContext extended with `user` and `roles`** — `context.user: User | undefined` and `context.roles: string[] | undefined` are populated before each resolver via `plugin.updateContext`. +69. **MercuriusContext extended with `user` and `roles`** — `context.user: User | undefined` and `context.roles: string[] | undefined` are populated before each resolver via `plugin.updateContext`. -69. **`@auth` directive** — protects a field or mutation; checks (1) authenticated session, (2) non-disabled account, (3) email verified (if enabled, unless `emailVerification: false` is passed), (4) profile complete (if enabled, unless `profileValidation: false` is passed). +70. **`@auth` directive** — protects a field or mutation; checks (1) authenticated session, (2) non-disabled account, (3) email verified (if enabled, unless `emailVerification: false` is passed), (4) profile complete (if enabled, unless `profileValidation: false` is passed). -70. **`@hasPermission(permission)` directive** — enforces a named permission on a GraphQL field; SUPERADMIN bypasses automatically. +71. **`@hasPermission(permission)` directive** — enforces a named permission on a GraphQL field; SUPERADMIN bypasses automatically. -71. **User GraphQL types** — `User`, `Photo`, `Users` (paginated wrapper with `totalCount`, `filteredCount`, `data`). +72. **User GraphQL types** — `User`, `Photo`, `Users` (paginated wrapper with `totalCount`, `filteredCount`, `data`). -72. **User queries** — `canAdminSignUp`, `me`, `user(id)`, `users(limit, offset, filters, sort)`. +73. **User queries** — `canAdminSignUp`, `me`, `user(id)`, `users(limit, offset, filters, sort)`. -73. **User mutations** — `adminSignUp`, `changeEmail`, `changePassword`, `deleteMe`, `disableUser`, `enableUser`, `removePhoto`, `updateMe`, `uploadPhoto`. The `uploadPhoto` mutation requires the GraphQL upload transport from `@prefabs.tech/fastify-graphql` (registered by default when the graphql plugin is enabled; configured via its `uploads` option). +74. **User mutations** — `adminSignUp`, `changeEmail`, `changePassword`, `deleteMe`, `disableUser`, `enableUser`, `removePhoto`, `updateMe`, `uploadPhoto`. The `uploadPhoto` mutation requires the GraphQL upload transport from `@prefabs.tech/fastify-graphql` (registered by default when the graphql plugin is enabled; configured via its `uploads` option). -74. **Invitation GraphQL types and operations** — `Invitation` type; queries `getInvitationByToken`, `listInvitation`; mutations `acceptInvitation`, `createInvitation`, `deleteInvitation`, `resendInvitation`, `revokeInvitation`. +75. **Invitation GraphQL types and operations** — `Invitation` type; queries `getInvitationByToken`, `listInvitation`; mutations `acceptInvitation`, `createInvitation`, `deleteInvitation`, `resendInvitation`, `revokeInvitation`. -75. **Role GraphQL types and operations** — `Role` type; queries `roles`, `rolePermissions`; mutations `createRole`, `deleteRole`, `updateRolePermissions`. +76. **Role GraphQL types and operations** — `Role` type; queries `roles`, `rolePermissions`; mutations `createRole`, `deleteRole`, `updateRolePermissions`. -76. **`permissions` GraphQL query** — returns the configured permission strings. +77. **`permissions` GraphQL query** — returns the configured permission strings. -77. **`userSchema` merged schema export** — the complete SDL string combining all user, invitation, role, and permission type definitions; ready to pass to `mergeTypeDefs`. +78. **`userSchema` merged schema export** — the complete SDL string combining all user, invitation, role, and permission type definitions; ready to pass to `mergeTypeDefs`. -78. **Resolver exports** — `userResolver`, `invitationResolver`, `roleResolver`, `permissionResolver` are exported individually for spreading into a larger resolver map. +79. **Resolver exports** — `userResolver`, `invitationResolver`, `roleResolver`, `permissionResolver` are exported individually for spreading into a larger resolver map. diff --git a/packages/user/GUIDE.md b/packages/user/GUIDE.md index 6d365bd87..ab6bf2c41 100644 --- a/packages/user/GUIDE.md +++ b/packages/user/GUIDE.md @@ -250,6 +250,33 @@ user: { For `override.apis` and `override.functions`, provide a function `(originalImpl, fastify) => partialOverride`; only the keys you return are replaced. +### Contributing a SuperTokens recipe from another plugin + +SuperTokens allows exactly one global `supertokens.init()`, and this package performs it synchronously while it is being registered — its recipe list is fixed at that moment. A plugin that wants to add a recipe of its own registers a factory instead: + +```typescript +import { addSupertokensRecipe } from "@prefabs.tech/fastify-user"; +import FastifyPlugin from "fastify-plugin"; +import Passwordless from "supertokens-node/recipe/passwordless"; + +const myRecipePlugin = async (fastify) => { + addSupertokensRecipe(fastify, (fastify) => + Passwordless.init({ contactMethod: "PHONE", flowType: "USER_INPUT_CODE" }), + ); +}; + +export default FastifyPlugin(myRecipePlugin); +``` + +Factories accumulate on the `fastify.supertokensRecipes` decorator and are drained by `getRecipeList` during `supertokens.init()`. **The contributing plugin must be registered before `@prefabs.tech/fastify-user`:** + +```typescript +await fastify.register(myRecipePlugin); +await fastify.register(userPlugin); +``` + +Registering it afterwards throws `SuperTokens is already initialised. Register SuperTokens recipe plugins before @prefabs.tech/fastify-user.` rather than silently dropping the recipe. `@prefabs.tech/fastify-phone-auth` is built on this hook. + ### Third-party OAuth providers Configure Apple, Facebook, GitHub, and Google via `config.user.supertokens.providers`: diff --git a/packages/user/src/index.ts b/packages/user/src/index.ts index 86454c421..27037b896 100644 --- a/packages/user/src/index.ts +++ b/packages/user/src/index.ts @@ -1,3 +1,4 @@ +import type { SupertokensRecipeFactory } from "./supertokens/types"; import type { User, UserConfig } from "./types"; import hasPermission from "./middlewares/hasPermission"; @@ -5,6 +6,8 @@ import hasPermission from "./middlewares/hasPermission"; declare module "fastify" { interface FastifyInstance { hasPermission: typeof hasPermission; + supertokensInitialized?: boolean; + supertokensRecipes?: SupertokensRecipeFactory[]; } interface FastifyRequest { @@ -58,6 +61,8 @@ export { export { default as UserSqlFactory } from "./model/users/sqlFactory"; export { default } from "./plugin"; export { errorHandler as supertokensErrorHandler } from "./supertokens/errorHandler"; +export { default as addSupertokensRecipe } from "./supertokens/recipeRegistry"; +export type { SupertokensRecipeFactory } from "./supertokens/types"; export { default as areRolesExist } from "./supertokens/utils/areRolesExist"; export { default as createUserContext } from "./supertokens/utils/createUserContext"; export { default as isRoleExists } from "./supertokens/utils/isRoleExists"; diff --git a/packages/user/src/supertokens/init.ts b/packages/user/src/supertokens/init.ts index 05fb5b751..b39c33c61 100644 --- a/packages/user/src/supertokens/init.ts +++ b/packages/user/src/supertokens/init.ts @@ -20,6 +20,8 @@ const init = (fastify: FastifyInstance) => { connectionURI: config.user.supertokens.connectionUri as string, }, }); + + fastify.decorate("supertokensInitialized", true); }; export default init; diff --git a/packages/user/src/supertokens/recipeRegistry.ts b/packages/user/src/supertokens/recipeRegistry.ts new file mode 100644 index 000000000..a4ffb58b2 --- /dev/null +++ b/packages/user/src/supertokens/recipeRegistry.ts @@ -0,0 +1,26 @@ +import type { FastifyInstance } from "fastify"; + +import type { SupertokensRecipeFactory } from "./types"; + +// SuperTokens allows exactly one global init(), which happens while +// @prefabs.tech/fastify-user is being registered. Plugins that contribute a +// recipe therefore have to be registered BEFORE it, so the factory is in the +// registry by the time getRecipeList() drains it. +const addSupertokensRecipe = ( + fastify: FastifyInstance, + factory: SupertokensRecipeFactory, +): void => { + if (fastify.hasDecorator("supertokensInitialized")) { + throw new Error( + "SuperTokens is already initialised. Register SuperTokens recipe plugins before @prefabs.tech/fastify-user.", + ); + } + + if (!fastify.hasDecorator("supertokensRecipes")) { + fastify.decorate("supertokensRecipes", []); + } + + fastify.supertokensRecipes?.push(factory); +}; + +export default addSupertokensRecipe; diff --git a/packages/user/src/supertokens/recipes/index.ts b/packages/user/src/supertokens/recipes/index.ts index 0c589bc40..83c8f448e 100644 --- a/packages/user/src/supertokens/recipes/index.ts +++ b/packages/user/src/supertokens/recipes/index.ts @@ -17,6 +17,12 @@ const getRecipeList = (fastify: FastifyInstance): RecipeListFunction[] => { recipeList.push(initEmailVerificationRecipe(fastify)); } + const registeredRecipes = fastify.supertokensRecipes ?? []; + + for (const factory of registeredRecipes) { + recipeList.push(factory(fastify)); + } + return recipeList; }; diff --git a/packages/user/src/supertokens/types/index.ts b/packages/user/src/supertokens/types/index.ts index a7df5c2ee..2aef8aaeb 100644 --- a/packages/user/src/supertokens/types/index.ts +++ b/packages/user/src/supertokens/types/index.ts @@ -4,6 +4,7 @@ import type { TypeInput as SessionRecipeConfig } from "supertokens-node/recipe/s import type { TypeProvider } from "supertokens-node/recipe/thirdpartyemailpassword"; import type { TypeInput as ThirdPartyEmailPasswordRecipeConfig } from "supertokens-node/recipe/thirdpartyemailpassword/types"; import type { TypeInput as UserRolesRecipeConfig } from "supertokens-node/recipe/userroles/types"; +import type { RecipeListFunction } from "supertokens-node/types"; import { Apple, @@ -32,6 +33,10 @@ interface SupertokensConfig { setErrorHandler?: boolean; } +type SupertokensRecipeFactory = ( + fastify: FastifyInstance, +) => RecipeListFunction; + interface SupertokensRecipes { emailVerification?: | ((fastify: FastifyInstance) => EmailVerificationRecipeConfig) @@ -51,4 +56,4 @@ interface SupertokensThirdPartyProvider { google?: Parameters[0]; } -export type { SupertokensConfig, SupertokensRecipes }; +export type { SupertokensConfig, SupertokensRecipeFactory, SupertokensRecipes }; diff --git a/packages/user/src/types/config.ts b/packages/user/src/types/config.ts index fb31ad4ee..2c8914d28 100644 --- a/packages/user/src/types/config.ts +++ b/packages/user/src/types/config.ts @@ -17,6 +17,7 @@ interface EmailOptions { subject?: string; templateName?: string; } + interface UserConfig { email?: IsEmailOptions; emailOverrides?: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 74f5a03ac..84b638f72 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -74,10 +74,10 @@ importers: version: 5.9.3 vite: specifier: 8.1.5 - version: 8.1.5(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.8.1) + version: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.8.1) vitest: specifier: 4.1.10 - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-istanbul@4.1.10)(vite@8.1.5(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.8.1)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-istanbul@4.1.10)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.8.1)) packages/error-handler: dependencies: @@ -120,10 +120,10 @@ importers: version: 5.9.3 vite: specifier: 8.1.5 - version: 8.1.5(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.8.1) + version: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.8.1) vitest: specifier: 4.1.10 - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-istanbul@4.1.10)(vite@8.1.5(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.8.1)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-istanbul@4.1.10)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.8.1)) packages/firebase: dependencies: @@ -190,10 +190,10 @@ importers: version: 5.9.3 vite: specifier: 8.1.5 - version: 8.1.5(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.8.1) + version: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.8.1) vitest: specifier: 4.1.10 - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-istanbul@4.1.10)(vite@8.1.5(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.8.1)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-istanbul@4.1.10)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.8.1)) packages/graphql: dependencies: @@ -257,10 +257,10 @@ importers: version: 5.9.3 vite: specifier: 8.1.5 - version: 8.1.5(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.8.1) + version: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.8.1) vitest: specifier: 4.1.10 - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-istanbul@4.1.10)(vite@8.1.5(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.8.1)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-istanbul@4.1.10)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.8.1)) zod: specifier: 3.25.76 version: 3.25.76 @@ -327,10 +327,68 @@ importers: version: 5.9.3 vite: specifier: 8.1.5 - version: 8.1.5(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.8.1) + version: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.8.1) vitest: specifier: 4.1.10 - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-istanbul@4.1.10)(vite@8.1.5(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.8.1)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-istanbul@4.1.10)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.8.1)) + + packages/phone-auth: + dependencies: + twilio: + specifier: 6.0.0 + version: 6.0.0 + devDependencies: + '@prefabs.tech/eslint-config': + specifier: 0.8.7 + version: 0.8.7(@typescript-eslint/parser@8.58.0(eslint@10.7.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.7.0(jiti@2.6.1))(prettier@3.9.5)(typescript@5.9.3) + '@prefabs.tech/fastify-config': + specifier: 0.94.1 + version: link:../config + '@prefabs.tech/fastify-error-handler': + specifier: 0.94.1 + version: link:../error-handler + '@prefabs.tech/fastify-slonik': + specifier: 0.94.1 + version: link:../slonik + '@prefabs.tech/fastify-user': + specifier: 0.94.1 + version: link:../user + '@prefabs.tech/tsconfig': + specifier: 0.8.7 + version: 0.8.7 + '@types/node': + specifier: 24.13.3 + version: 24.13.3 + '@vitest/coverage-istanbul': + specifier: 3.2.7 + version: 3.2.7(vitest@3.2.7(@types/node@24.13.3)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.1)) + eslint: + specifier: 10.7.0 + version: 10.7.0(jiti@2.6.1) + fastify: + specifier: 5.10.0 + version: 5.10.0 + fastify-plugin: + specifier: 6.0.0 + version: 6.0.0 + prettier: + specifier: 3.9.5 + version: 3.9.5 + slonik: + specifier: 46.8.0 + version: 46.8.0(zod@3.25.76) + supertokens-node: + specifier: 14.1.4 + version: 14.1.4 + typescript: + specifier: 5.9.3 + version: 5.9.3 + vite: + specifier: 8.1.5 + version: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.8.1) + vitest: + specifier: 3.2.7 + version: 3.2.7(@types/node@24.13.3)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.1) packages/s3: dependencies: @@ -400,10 +458,10 @@ importers: version: 5.9.3 vite: specifier: 8.1.5 - version: 8.1.5(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.8.1) + version: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.8.1) vitest: specifier: 4.1.10 - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-istanbul@4.1.10)(vite@8.1.5(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.8.1)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-istanbul@4.1.10)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.8.1)) zod: specifier: 3.25.76 version: 3.25.76 @@ -470,10 +528,10 @@ importers: version: 5.9.3 vite: specifier: 8.1.5 - version: 8.1.5(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.8.1) + version: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.8.1) vitest: specifier: 4.1.10 - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-istanbul@4.1.10)(vite@8.1.5(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.8.1)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-istanbul@4.1.10)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.8.1)) zod: specifier: 3.25.76 version: 3.25.76 @@ -516,10 +574,10 @@ importers: version: 5.9.3 vite: specifier: 8.1.5 - version: 8.1.5(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.8.1) + version: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.8.1) vitest: specifier: 4.1.10 - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-istanbul@4.1.10)(vite@8.1.5(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.8.1)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-istanbul@4.1.10)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.8.1)) packages/user: dependencies: @@ -598,10 +656,10 @@ importers: version: 5.9.3 vite: specifier: 8.1.5 - version: 8.1.5(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.8.1) + version: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.8.1) vitest: specifier: 4.1.10 - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-istanbul@4.1.10)(vite@8.1.5(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.8.1)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-istanbul@4.1.10)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.8.1)) zod: specifier: 3.25.76 version: 3.25.76 @@ -690,10 +748,6 @@ packages: resolution: {integrity: sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==} engines: {node: '>=18.0.0'} - '@babel/code-frame@7.27.1': - resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} - engines: {node: '>=6.9.0'} - '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} @@ -935,21 +989,168 @@ packages: '@emnapi/core@1.11.1': resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} - '@emnapi/core@1.8.1': - resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} - '@emnapi/runtime@1.11.1': resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} - '@emnapi/runtime@1.8.1': - resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} - - '@emnapi/wasi-threads@1.1.0': - resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} - '@emnapi/wasi-threads@1.2.2': resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.9.1': resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1329,14 +1530,6 @@ packages: '@types/node': optional: true - '@isaacs/balanced-match@4.0.1': - resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} - engines: {node: 20 || >=22} - - '@isaacs/brace-expansion@5.0.0': - resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} - engines: {node: 20 || >=22} - '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -1372,6 +1565,13 @@ packages: resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} engines: {node: '>=8'} + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + libc: [glibc] + '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} @@ -1457,10 +1657,6 @@ packages: '@one-ini/wasm@0.1.1': resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} - '@opentelemetry/api@1.9.0': - resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} - engines: {node: '>=8.0.0'} - '@opentelemetry/api@1.9.1': resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} engines: {node: '>=8.0.0'} @@ -1622,6 +1818,144 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@rollup/rollup-android-arm-eabi@4.62.4': + resolution: {integrity: sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.62.4': + resolution: {integrity: sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.62.4': + resolution: {integrity: sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.62.4': + resolution: {integrity: sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.62.4': + resolution: {integrity: sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.62.4': + resolution: {integrity: sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.62.4': + resolution: {integrity: sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.62.4': + resolution: {integrity: sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.62.4': + resolution: {integrity: sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.62.4': + resolution: {integrity: sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.62.4': + resolution: {integrity: sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.62.4': + resolution: {integrity: sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.62.4': + resolution: {integrity: sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.62.4': + resolution: {integrity: sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.62.4': + resolution: {integrity: sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.62.4': + resolution: {integrity: sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.62.4': + resolution: {integrity: sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.62.4': + resolution: {integrity: sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.62.4': + resolution: {integrity: sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.62.4': + resolution: {integrity: sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.62.4': + resolution: {integrity: sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.62.4': + resolution: {integrity: sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.62.4': + resolution: {integrity: sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.62.4': + resolution: {integrity: sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.62.4': + resolution: {integrity: sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==} + cpu: [x64] + os: [win32] + '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -1754,9 +2088,6 @@ packages: cpu: [arm64] os: [win32] - '@tybys/wasm-util@0.10.1': - resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} - '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} @@ -1778,6 +2109,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/html-to-text@9.0.4': resolution: {integrity: sha512-pUY3cKH/Nm2yYrEmDlPR1mR7yszjGx4DrwPjQ702C4/D5CwHuZTgZdIdwPkRbcuhs7BAh2L5rg3CL5cbRiGTCQ==} @@ -2019,14 +2353,33 @@ packages: cpu: [x64] os: [win32] + '@vitest/coverage-istanbul@3.2.7': + resolution: {integrity: sha512-/o+QIJBJCBZm7FdyAMqYKvr8Cd0YsqPt9UwVT6bAA+U9Ae8AR39ZAjq7xnmKCO64TkzPm0Vvi7jzYZWLdu6Y5A==} + peerDependencies: + vitest: 3.2.7 + '@vitest/coverage-istanbul@4.1.10': resolution: {integrity: sha512-AyNJ5pQRFqCX7pwB9PSTmoVKPaZ4H5IEVJfJsT+q1DYkXvZMEFYgJlyk5sfStmt9rVYRyYYRRsuBeImCOc39ww==} peerDependencies: vitest: 4.1.10 + '@vitest/expect@3.2.7': + resolution: {integrity: sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==} + '@vitest/expect@4.1.10': resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} + '@vitest/mocker@3.2.7': + resolution: {integrity: sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + '@vitest/mocker@4.1.10': resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} peerDependencies: @@ -2038,18 +2391,33 @@ packages: vite: optional: true + '@vitest/pretty-format@3.2.7': + resolution: {integrity: sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==} + '@vitest/pretty-format@4.1.10': resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} + '@vitest/runner@3.2.7': + resolution: {integrity: sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==} + '@vitest/runner@4.1.10': resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} + '@vitest/snapshot@3.2.7': + resolution: {integrity: sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==} + '@vitest/snapshot@4.1.10': resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} + '@vitest/spy@3.2.7': + resolution: {integrity: sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==} + '@vitest/spy@4.1.10': resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} + '@vitest/utils@3.2.7': + resolution: {integrity: sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==} + '@vitest/utils@4.1.10': resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} @@ -2209,9 +2577,6 @@ packages: resolution: {integrity: sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==} engines: {node: '>=4'} - axios@1.12.2: - resolution: {integrity: sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==} - axios@1.13.5: resolution: {integrity: sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==} @@ -2234,10 +2599,6 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - baseline-browser-mapping@2.8.20: - resolution: {integrity: sha512-JMWsdF+O8Orq3EMukbUN1QfbLK9mX2CkUmQBcW2T0s8OmdAUL5LLM/6wFwSrqXzlXB13yhyK9gTKS1rIizOduQ==} - hasBin: true - before-after-hook@3.0.2: resolution: {integrity: sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==} @@ -2271,11 +2632,6 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.27.0: - resolution: {integrity: sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - browserslist@4.28.5: resolution: {integrity: sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -2306,6 +2662,10 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -2332,12 +2692,13 @@ packages: caniuse-api@3.0.0: resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} - caniuse-lite@1.0.30001751: - resolution: {integrity: sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==} - caniuse-lite@1.0.30001803: resolution: {integrity: sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==} + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} @@ -2352,6 +2713,10 @@ packages: chardet@2.1.1: resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + cheerio-select@2.1.0: resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} @@ -2638,6 +3003,10 @@ packages: supports-color: optional: true + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -2757,9 +3126,6 @@ packages: engines: {node: '>=0.10.0'} hasBin: true - electron-to-chromium@1.5.240: - resolution: {integrity: sha512-OBwbZjWgrCOH+g6uJsA2/7Twpas2OlepS9uvByJjR2datRDuKGYeD+nP8lBBks2qnB7bGJNHDUx7c/YLaT3QMQ==} - electron-to-chromium@1.5.389: resolution: {integrity: sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==} @@ -2820,6 +3186,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-module-lexer@2.3.1: resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} @@ -2842,6 +3211,11 @@ packages: es-toolkit@1.46.1: resolution: {integrity: sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==} + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -3875,10 +4249,18 @@ packages: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} + istanbul-lib-instrument@6.0.3: + resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} + engines: {node: '>=10'} + istanbul-lib-report@3.0.1: resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} engines: {node: '>=10'} + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + istanbul-reports@3.2.0: resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} engines: {node: '>=8'} @@ -3914,6 +4296,9 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + js-yaml@4.1.1: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true @@ -3967,10 +4352,6 @@ packages: jsonify@0.0.1: resolution: {integrity: sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==} - jsonwebtoken@9.0.2: - resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} - engines: {node: '>=12', npm: '>=6'} - jsonwebtoken@9.0.3: resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} engines: {node: '>=12', npm: '>=6'} @@ -3989,9 +4370,6 @@ packages: engines: {node: '>=18.17'} hasBin: true - jwa@1.4.2: - resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==} - jwa@2.0.1: resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} @@ -3999,9 +4377,6 @@ packages: resolution: {integrity: sha512-BqTyEDV+lS8F2trk3A+qJnxV5Q9EqKCBJOPti3W97r7qTympCZjb7h2X6f2kc+0K3rsSTY1/6YG2eaXKoj497w==} engines: {node: '>=14'} - jws@3.2.2: - resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} - jws@4.0.1: resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} @@ -4164,6 +4539,9 @@ packages: long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lower-case@1.1.4: resolution: {integrity: sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==} @@ -4187,6 +4565,9 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + magicast@0.3.5: + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + magicast@0.5.3: resolution: {integrity: sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==} @@ -4262,10 +4643,6 @@ packages: engines: {node: '>=10.0.0'} hasBin: true - minimatch@10.1.1: - resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} - engines: {node: 20 || >=22} - minimatch@10.2.5: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} @@ -4552,9 +4929,6 @@ packages: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - node-releases@2.0.26: - resolution: {integrity: sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==} - node-releases@2.0.51: resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==} engines: {node: '>=18'} @@ -4749,6 +5123,10 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + peberminta@0.10.0: resolution: {integrity: sha512-80B2AsU+I4Qdb0ZAPSfe9UwvGzwkM37IKIFEvdS3D/3Ndgv2bsuJ0bfG1+iEYO+l7Gfd4EUJmuRyq7efLgRMzQ==} @@ -4852,10 +5230,6 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} - picomatch@4.0.3: - resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} - engines: {node: '>=12'} - picomatch@4.0.5: resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} engines: {node: '>=12'} @@ -5166,8 +5540,8 @@ packages: resolution: {integrity: sha512-O+Wd1chXj5YE1DwmD+ae0bXiSLehmnS3czlC1R9FL/Nt/3q8uMS1bIHmg2lJfCoiimCxClWM8AAuJrF0EvNiog==} engines: {node: '>= 16'} - qs@6.14.0: - resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + qs@6.15.3: + resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} engines: {node: '>=0.6'} querystringify@2.2.0: @@ -5300,6 +5674,11 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + rollup@4.62.4: + resolution: {integrity: sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + run-applescript@7.1.0: resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} engines: {node: '>=18'} @@ -5346,6 +5725,7 @@ packages: scmp@2.1.0: resolution: {integrity: sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==} + deprecated: Just use Node.js's crypto.timingSafeEqual() secure-json-parse@4.1.0: resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} @@ -5420,8 +5800,8 @@ packages: engines: {node: '>=20'} hasBin: true - side-channel-list@1.0.0: - resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} engines: {node: '>= 0.4'} side-channel-map@1.0.1: @@ -5432,8 +5812,8 @@ packages: resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} engines: {node: '>= 0.4'} - side-channel@1.1.0: - resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} engines: {node: '>= 0.4'} siginfo@2.0.0: @@ -5514,6 +5894,9 @@ packages: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + std-env@4.2.0: resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} @@ -5591,6 +5974,9 @@ packages: resolution: {integrity: sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==} engines: {node: '>=12'} + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + strnum@2.2.3: resolution: {integrity: sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==} @@ -5650,6 +6036,10 @@ packages: resolution: {integrity: sha512-bX655WZI/F7EoTDw9JvQURqAXiPHi8o8+yFxPF2lWYyz1aHnmMRuXWqL6YB6GmeO0o4DIYWHLgGNi/X64T+X4Q==} engines: {node: '>=14.18'} + test-exclude@7.0.2: + resolution: {integrity: sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==} + engines: {node: '>=18'} + thread-stream@4.0.0: resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==} engines: {node: '>=20'} @@ -5665,22 +6055,33 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyexec@1.1.2: resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==} engines: {node: '>=18'} - tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} - engines: {node: '>=12.0.0'} - tinyglobby@0.2.17: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + tinyrainbow@3.1.0: resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -5719,6 +6120,10 @@ packages: resolution: {integrity: sha512-LdNBQfOe0dY2oJH2sAsrxazpgfFQo5yXGxe96QA8UWB5uu+433PrUbkv8gQ5RmrRCqUTPQ0aOrIyAdBr1aB03Q==} engines: {node: '>=14.0'} + twilio@6.0.0: + resolution: {integrity: sha512-MAie5DJ3KLpcKlDaYtNzsKMQXcCi+YHWKvZjuSpm27vJAO/l8PanJA0LkkJ03sbh+Kwe5NeL0Q2+y6IjNUYeUA==} + engines: {node: '>=20.0.0'} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -5800,12 +6205,6 @@ packages: unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} - update-browserslist-db@1.1.4: - resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -5853,6 +6252,51 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@7.3.6: + resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + vite@8.1.5: resolution: {integrity: sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5896,6 +6340,34 @@ packages: yaml: optional: true + vitest@3.2.7: + resolution: {integrity: sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.7 + '@vitest/ui': 3.2.7 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vitest@4.1.10: resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -6302,15 +6774,9 @@ snapshots: '@aws/lambda-invoke-store@0.3.0': {} - '@babel/code-frame@7.27.1': - dependencies: - '@babel/helper-validator-identifier': 7.28.5 - js-tokens: 4.0.0 - picocolors: 1.1.1 - '@babel/code-frame@7.29.0': dependencies: - '@babel/helper-validator-identifier': 7.28.5 + '@babel/helper-validator-identifier': 7.29.7 js-tokens: 4.0.0 picocolors: 1.1.1 @@ -6326,7 +6792,7 @@ snapshots: '@babel/core@7.28.5': dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.29.0 '@babel/generator': 7.28.5 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) @@ -6384,7 +6850,7 @@ snapshots: dependencies: '@babel/compat-data': 7.28.5 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.27.0 + browserslist: 4.28.5 lru-cache: 5.1.1 semver: 6.3.1 @@ -6403,7 +6869,7 @@ snapshots: '@babel/helper-module-imports@7.27.1': dependencies: '@babel/traverse': 7.28.5 - '@babel/types': 7.28.5 + '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color @@ -6466,7 +6932,7 @@ snapshots: '@babel/template@7.27.2': dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.29.0 '@babel/parser': 7.28.5 '@babel/types': 7.28.5 @@ -6478,7 +6944,7 @@ snapshots: '@babel/traverse@7.28.5': dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.29.0 '@babel/generator': 7.28.5 '@babel/helper-globals': 7.28.0 '@babel/parser': 7.28.5 @@ -6644,30 +7110,92 @@ snapshots: tslib: 2.8.1 optional: true - '@emnapi/core@1.8.1': - dependencies: - '@emnapi/wasi-threads': 1.1.0 - tslib: 2.8.1 + '@emnapi/runtime@1.11.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.28.1': + optional: true + + '@esbuild/android-arm64@0.28.1': + optional: true + + '@esbuild/android-arm@0.28.1': + optional: true + + '@esbuild/android-x64@0.28.1': + optional: true + + '@esbuild/darwin-arm64@0.28.1': + optional: true + + '@esbuild/darwin-x64@0.28.1': + optional: true + + '@esbuild/freebsd-arm64@0.28.1': + optional: true + + '@esbuild/freebsd-x64@0.28.1': + optional: true + + '@esbuild/linux-arm64@0.28.1': + optional: true + + '@esbuild/linux-arm@0.28.1': + optional: true + + '@esbuild/linux-ia32@0.28.1': + optional: true + + '@esbuild/linux-loong64@0.28.1': + optional: true + + '@esbuild/linux-mips64el@0.28.1': + optional: true + + '@esbuild/linux-ppc64@0.28.1': + optional: true + + '@esbuild/linux-riscv64@0.28.1': + optional: true + + '@esbuild/linux-s390x@0.28.1': + optional: true + + '@esbuild/linux-x64@0.28.1': + optional: true + + '@esbuild/netbsd-arm64@0.28.1': + optional: true + + '@esbuild/netbsd-x64@0.28.1': + optional: true + + '@esbuild/openbsd-arm64@0.28.1': + optional: true + + '@esbuild/openbsd-x64@0.28.1': + optional: true + + '@esbuild/openharmony-arm64@0.28.1': optional: true - '@emnapi/runtime@1.11.1': - dependencies: - tslib: 2.8.1 + '@esbuild/sunos-x64@0.28.1': optional: true - '@emnapi/runtime@1.8.1': - dependencies: - tslib: 2.8.1 + '@esbuild/win32-arm64@0.28.1': optional: true - '@emnapi/wasi-threads@1.1.0': - dependencies: - tslib: 2.8.1 + '@esbuild/win32-ia32@0.28.1': optional: true - '@emnapi/wasi-threads@1.2.2': - dependencies: - tslib: 2.8.1 + '@esbuild/win32-x64@0.28.1': optional: true '@eslint-community/eslint-utils@4.9.1(eslint@10.7.0(jiti@2.6.1))': @@ -7165,12 +7693,6 @@ snapshots: optionalDependencies: '@types/node': 24.10.15 - '@isaacs/balanced-match@4.0.1': {} - - '@isaacs/brace-expansion@5.0.0': - dependencies: - '@isaacs/balanced-match': 4.0.1 - '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -7208,11 +7730,14 @@ snapshots: '@lukeed/ms@2.0.2': {} + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + '@napi-rs/wasm-runtime@0.2.12': dependencies: - '@emnapi/core': 1.8.1 - '@emnapi/runtime': 1.8.1 - '@tybys/wasm-util': 0.10.1 + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 optional: true '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': @@ -7307,10 +7832,7 @@ snapshots: '@one-ini/wasm@0.1.1': {} - '@opentelemetry/api@1.9.0': {} - - '@opentelemetry/api@1.9.1': - optional: true + '@opentelemetry/api@1.9.1': {} '@oxc-project/types@0.139.0': {} @@ -7445,6 +7967,81 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} + '@rollup/rollup-android-arm-eabi@4.62.4': + optional: true + + '@rollup/rollup-android-arm64@4.62.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.62.4': + optional: true + + '@rollup/rollup-darwin-x64@4.62.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.62.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.62.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.62.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.62.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.62.4': + optional: true + + '@rollup/rollup-openbsd-x64@4.62.4': + optional: true + + '@rollup/rollup-openharmony-arm64@4.62.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.62.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.62.4': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.62.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.62.4': + optional: true + '@rtsao/scc@1.1.0': {} '@sec-ant/readable-stream@0.4.1': {} @@ -7471,7 +8068,7 @@ snapshots: dependencies: '@slack/types': 2.20.0 '@types/node': 24.13.3 - axios: 1.13.5 + axios: 1.13.5(debug@4.4.3) transitivePeerDependencies: - debug @@ -7579,11 +8176,6 @@ snapshots: '@turbo/windows-arm64@2.10.6': optional: true - '@tybys/wasm-util@0.10.1': - dependencies: - tslib: 2.8.1 - optional: true - '@tybys/wasm-util@0.10.3': dependencies: tslib: 2.8.1 @@ -7607,6 +8199,8 @@ snapshots: '@types/estree@1.0.8': {} + '@types/estree@1.0.9': {} + '@types/html-to-text@9.0.4': {} '@types/humps@2.0.6': {} @@ -7761,7 +8355,7 @@ snapshots: debug: 4.4.3 minimatch: 10.2.5 semver: 7.8.5 - tinyglobby: 0.2.15 + tinyglobby: 0.2.17 ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: @@ -7847,6 +8441,22 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true + '@vitest/coverage-istanbul@3.2.7(vitest@3.2.7(@types/node@24.13.3)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.1))': + dependencies: + '@istanbuljs/schema': 0.1.3 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + magicast: 0.3.5 + test-exclude: 7.0.2 + tinyrainbow: 2.0.0 + vitest: 3.2.7(@types/node@24.13.3)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.1) + transitivePeerDependencies: + - supports-color + '@vitest/coverage-istanbul@4.1.10(vitest@4.1.10)': dependencies: '@babel/core': 7.29.7 @@ -7859,10 +8469,18 @@ snapshots: magicast: 0.5.3 obug: 2.1.4 tinyrainbow: 3.1.0 - vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-istanbul@4.1.10)(vite@8.1.5(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.8.1)) + vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-istanbul@4.1.10)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.8.1)) transitivePeerDependencies: - supports-color + '@vitest/expect@3.2.7': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.7 + '@vitest/utils': 3.2.7 + chai: 5.3.3 + tinyrainbow: 2.0.0 + '@vitest/expect@4.1.10': dependencies: '@standard-schema/spec': 1.1.0 @@ -7872,23 +8490,47 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.10(vite@8.1.5(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.8.1))': + '@vitest/mocker@3.2.7(vite@7.3.6(@types/node@24.13.3)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.1))': + dependencies: + '@vitest/spy': 3.2.7 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.6(@types/node@24.13.3)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.1) + + '@vitest/mocker@4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.8.1))': dependencies: '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.1.5(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.8.1) + vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.8.1) + + '@vitest/pretty-format@3.2.7': + dependencies: + tinyrainbow: 2.0.0 '@vitest/pretty-format@4.1.10': dependencies: tinyrainbow: 3.1.0 + '@vitest/runner@3.2.7': + dependencies: + '@vitest/utils': 3.2.7 + pathe: 2.0.3 + strip-literal: 3.1.0 + '@vitest/runner@4.1.10': dependencies: '@vitest/utils': 4.1.10 pathe: 2.0.3 + '@vitest/snapshot@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + magic-string: 0.30.21 + pathe: 2.0.3 + '@vitest/snapshot@4.1.10': dependencies: '@vitest/pretty-format': 4.1.10 @@ -7896,8 +8538,18 @@ snapshots: magic-string: 0.30.21 pathe: 2.0.3 + '@vitest/spy@3.2.7': + dependencies: + tinyspy: 4.0.4 + '@vitest/spy@4.1.10': {} + '@vitest/utils@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + '@vitest/utils@4.1.10': dependencies: '@vitest/pretty-format': 4.1.10 @@ -8067,15 +8719,7 @@ snapshots: axe-core@4.11.1: {} - axios@1.12.2(debug@4.4.3): - dependencies: - follow-redirects: 1.15.11(debug@4.4.3) - form-data: 4.0.5 - proxy-from-env: 1.1.0 - transitivePeerDependencies: - - debug - - axios@1.13.5: + axios@1.13.5(debug@4.4.3): dependencies: follow-redirects: 1.15.11(debug@4.4.3) form-data: 4.0.5 @@ -8093,8 +8737,6 @@ snapshots: baseline-browser-mapping@2.10.42: {} - baseline-browser-mapping@2.8.20: {} - before-after-hook@3.0.2: {} bignumber.js@9.3.1: {} @@ -8124,14 +8766,6 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.27.0: - dependencies: - baseline-browser-mapping: 2.8.20 - caniuse-lite: 1.0.30001751 - electron-to-chromium: 1.5.240 - node-releases: 2.0.26 - update-browserslist-db: 1.1.4(browserslist@4.27.0) - browserslist@4.28.5: dependencies: baseline-browser-mapping: 2.10.42 @@ -8164,6 +8798,8 @@ snapshots: bytes@3.1.2: {} + cac@6.7.14: {} + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -8202,10 +8838,16 @@ snapshots: lodash.memoize: 4.1.2 lodash.uniq: 4.5.0 - caniuse-lite@1.0.30001751: {} - caniuse-lite@1.0.30001803: {} + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + chai@6.2.2: {} chalk@5.6.2: {} @@ -8214,6 +8856,8 @@ snapshots: chardet@2.1.1: {} + check-error@2.1.3: {} + cheerio-select@2.1.0: dependencies: boolbase: 1.0.0 @@ -8535,6 +9179,8 @@ snapshots: dependencies: ms: 2.1.3 + deep-eql@5.0.2: {} + deep-is@0.1.4: {} deepmerge-ts@7.1.5: {} @@ -8654,8 +9300,6 @@ snapshots: dependencies: jake: 10.9.4 - electron-to-chromium@1.5.240: {} - electron-to-chromium@1.5.389: {} emoji-regex@10.6.0: {} @@ -8755,6 +9399,8 @@ snapshots: es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} + es-module-lexer@2.3.1: {} es-object-atoms@1.1.1: @@ -8780,6 +9426,35 @@ snapshots: es-toolkit@1.46.1: {} + esbuild@0.28.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 + escalade@3.2.0: {} escape-goat@3.0.0: {} @@ -8824,7 +9499,7 @@ snapshots: get-tsconfig: 4.13.1 is-bun-module: 2.0.0 stable-hash-x: 0.2.0 - tinyglobby: 0.2.15 + tinyglobby: 0.2.17 unrs-resolver: 1.11.1 optionalDependencies: eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@10.7.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.5)(eslint@10.7.0(jiti@2.6.1)) @@ -9284,10 +9959,6 @@ snapshots: dependencies: walk-up-path: 3.0.1 - fdir@6.5.0(picomatch@4.0.3): - optionalDependencies: - picomatch: 4.0.3 - fdir@6.5.0(picomatch@4.0.5): optionalDependencies: picomatch: 4.0.5 @@ -9378,7 +10049,7 @@ snapshots: asynckit: 0.4.0 combined-stream: 1.0.8 es-set-tostringtag: 2.1.0 - hasown: 2.0.2 + hasown: 2.0.3 mime-types: 2.1.35 formdata-polyfill@4.0.10: @@ -9543,7 +10214,7 @@ snapshots: glob@13.0.0: dependencies: - minimatch: 10.1.1 + minimatch: 10.2.5 minipass: 7.1.2 path-scurry: 2.0.0 @@ -9886,7 +10557,7 @@ snapshots: dependencies: es-errors: 1.3.0 hasown: 2.0.3 - side-channel: 1.1.0 + side-channel: 1.1.1 ipaddr.js@2.4.0: {} @@ -10052,12 +10723,30 @@ snapshots: istanbul-lib-coverage@3.2.2: {} + istanbul-lib-instrument@6.0.3: + dependencies: + '@babel/core': 7.29.7 + '@babel/parser': 7.29.7 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 7.8.5 + transitivePeerDependencies: + - supports-color + istanbul-lib-report@3.0.1: dependencies: istanbul-lib-coverage: 3.2.2 make-dir: 4.0.0 supports-color: 7.2.0 + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + istanbul-reports@3.2.0: dependencies: html-escaper: 2.0.2 @@ -10095,6 +10784,8 @@ snapshots: js-tokens@4.0.0: {} + js-tokens@9.0.1: {} + js-yaml@4.1.1: dependencies: argparse: 2.0.1 @@ -10147,19 +10838,6 @@ snapshots: jsonify@0.0.1: {} - jsonwebtoken@9.0.2: - dependencies: - jws: 3.2.2 - lodash.includes: 4.3.0 - lodash.isboolean: 3.0.3 - lodash.isinteger: 4.0.4 - lodash.isnumber: 3.0.3 - lodash.isplainobject: 4.0.6 - lodash.isstring: 4.0.1 - lodash.once: 4.1.1 - ms: 2.1.3 - semver: 7.7.3 - jsonwebtoken@9.0.3: dependencies: jws: 4.0.1 @@ -10199,12 +10877,6 @@ snapshots: slick: 1.12.2 web-resource-inliner: 8.0.0 - jwa@1.4.2: - dependencies: - buffer-equal-constant-time: 1.0.1 - ecdsa-sig-formatter: 1.0.11 - safe-buffer: 5.2.1 - jwa@2.0.1: dependencies: buffer-equal-constant-time: 1.0.1 @@ -10221,11 +10893,6 @@ snapshots: transitivePeerDependencies: - supports-color - jws@3.2.2: - dependencies: - jwa: 1.4.2 - safe-buffer: 5.2.1 - jws@4.0.1: dependencies: jwa: 2.0.1 @@ -10349,6 +11016,8 @@ snapshots: long@5.3.2: optional: true + loupe@3.2.1: {} + lower-case@1.1.4: {} lru-cache@10.4.3: {} @@ -10372,6 +11041,12 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + magicast@0.3.5: + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + source-map-js: 1.2.1 + magicast@0.5.3: dependencies: '@babel/parser': 7.29.7 @@ -10450,10 +11125,6 @@ snapshots: mime@3.0.0: {} - minimatch@10.1.1: - dependencies: - '@isaacs/brace-expansion': 5.0.0 - minimatch@10.2.5: dependencies: brace-expansion: 5.0.5 @@ -11282,8 +11953,6 @@ snapshots: fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 - node-releases@2.0.26: {} - node-releases@2.0.51: {} nodemailer-html-to-text@3.2.0: @@ -11479,6 +12148,8 @@ snapshots: pathe@2.0.3: {} + pathval@2.0.1: {} + peberminta@0.10.0: {} pg-cloudflare@1.4.0: @@ -11560,8 +12231,6 @@ snapshots: picomatch@2.3.1: {} - picomatch@4.0.3: {} - picomatch@4.0.5: {} pino-abstract-transport@3.0.0: @@ -11846,9 +12515,10 @@ snapshots: qlobber@8.0.1: {} - qs@6.14.0: + qs@6.15.3: dependencies: - side-channel: 1.1.0 + es-define-property: 1.0.1 + side-channel: 1.1.1 querystringify@2.2.0: {} @@ -11994,6 +12664,38 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.1.5 '@rolldown/binding-win32-x64-msvc': 1.1.5 + rollup@4.62.4: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@rollup/rollup-android-arm-eabi': 4.62.4 + '@rollup/rollup-android-arm64': 4.62.4 + '@rollup/rollup-darwin-arm64': 4.62.4 + '@rollup/rollup-darwin-x64': 4.62.4 + '@rollup/rollup-freebsd-arm64': 4.62.4 + '@rollup/rollup-freebsd-x64': 4.62.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.4 + '@rollup/rollup-linux-arm-musleabihf': 4.62.4 + '@rollup/rollup-linux-arm64-gnu': 4.62.4 + '@rollup/rollup-linux-arm64-musl': 4.62.4 + '@rollup/rollup-linux-loong64-gnu': 4.62.4 + '@rollup/rollup-linux-loong64-musl': 4.62.4 + '@rollup/rollup-linux-ppc64-gnu': 4.62.4 + '@rollup/rollup-linux-ppc64-musl': 4.62.4 + '@rollup/rollup-linux-riscv64-gnu': 4.62.4 + '@rollup/rollup-linux-riscv64-musl': 4.62.4 + '@rollup/rollup-linux-s390x-gnu': 4.62.4 + '@rollup/rollup-linux-x64-gnu': 4.62.4 + '@rollup/rollup-linux-x64-musl': 4.62.4 + '@rollup/rollup-openbsd-x64': 4.62.4 + '@rollup/rollup-openharmony-arm64': 4.62.4 + '@rollup/rollup-win32-arm64-msvc': 4.62.4 + '@rollup/rollup-win32-ia32-msvc': 4.62.4 + '@rollup/rollup-win32-x64-gnu': 4.62.4 + '@rollup/rollup-win32-x64-msvc': 4.62.4 + fsevents: 2.3.3 + run-applescript@7.1.0: {} run-async@4.0.6: {} @@ -12131,7 +12833,7 @@ snapshots: - conventional-commits-filter - debug - side-channel-list@1.0.0: + side-channel-list@1.0.1: dependencies: es-errors: 1.3.0 object-inspect: 1.13.4 @@ -12151,11 +12853,11 @@ snapshots: object-inspect: 1.13.4 side-channel-map: 1.0.1 - side-channel@1.1.0: + side-channel@1.1.1: dependencies: es-errors: 1.3.0 object-inspect: 1.13.4 - side-channel-list: 1.0.0 + side-channel-list: 1.0.1 side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 @@ -12180,7 +12882,7 @@ snapshots: slonik@46.8.0(zod@3.25.76): dependencies: - '@opentelemetry/api': 1.9.0 + '@opentelemetry/api': 1.9.1 '@slonik/driver': 46.8.0(zod@3.25.76) '@slonik/errors': 46.8.0(zod@3.25.76) '@slonik/pg-driver': 46.8.0(zod@3.25.76) @@ -12237,6 +12939,8 @@ snapshots: statuses@2.0.1: {} + std-env@3.10.0: {} + std-env@4.2.0: {} stop-iteration-iterator@1.1.0: @@ -12327,6 +13031,10 @@ snapshots: strip-indent@4.1.1: {} + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + strnum@2.2.3: optional: true @@ -12411,6 +13119,12 @@ snapshots: dependencies: temp-dir: 3.0.0 + test-exclude@7.0.2: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 10.4.5 + minimatch: 10.2.5 + thread-stream@4.0.0: dependencies: real-require: 0.2.0 @@ -12423,20 +13137,23 @@ snapshots: tinybench@2.9.0: {} - tinyexec@1.1.2: {} + tinyexec@0.3.2: {} - tinyglobby@0.2.15: - dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + tinyexec@1.1.2: {} tinyglobby@0.2.17: dependencies: fdir: 6.5.0(picomatch@4.0.5) picomatch: 4.0.5 + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + tinyrainbow@3.1.0: {} + tinyspy@4.0.4: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -12473,11 +13190,11 @@ snapshots: twilio@4.23.0(debug@4.4.3): dependencies: - axios: 1.12.2(debug@4.4.3) + axios: 1.13.5(debug@4.4.3) dayjs: 1.11.18 https-proxy-agent: 5.0.1 - jsonwebtoken: 9.0.2 - qs: 6.14.0 + jsonwebtoken: 9.0.3 + qs: 6.15.3 scmp: 2.1.0 url-parse: 1.5.10 xmlbuilder: 13.0.2 @@ -12485,6 +13202,19 @@ snapshots: - debug - supports-color + twilio@6.0.0: + dependencies: + axios: 1.13.5(debug@4.4.3) + dayjs: 1.11.18 + https-proxy-agent: 5.0.1 + jsonwebtoken: 9.0.3 + qs: 6.15.3 + scmp: 2.1.0 + xmlbuilder: 13.0.2 + transitivePeerDependencies: + - debug + - supports-color + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -12592,12 +13322,6 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 - update-browserslist-db@1.1.4(browserslist@4.27.0): - dependencies: - browserslist: 4.27.0 - escalade: 3.2.0 - picocolors: 1.1.1 - update-browserslist-db@1.2.3(browserslist@4.28.5): dependencies: browserslist: 4.28.5 @@ -12636,7 +13360,43 @@ snapshots: vary@1.1.2: {} - vite@8.1.5(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.8.1): + vite-node@3.2.4(@types/node@24.13.3)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.1): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.3.6(@types/node@24.13.3)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.1) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@7.3.6(@types/node@24.13.3)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.1): + dependencies: + esbuild: 0.28.1 + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + postcss: 8.5.19 + rollup: 4.62.4 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 24.13.3 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.32.0 + yaml: 2.8.1 + + vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.8.1): dependencies: lightningcss: 1.32.0 picomatch: 4.0.5 @@ -12645,14 +13405,56 @@ snapshots: tinyglobby: 0.2.17 optionalDependencies: '@types/node': 24.13.3 + esbuild: 0.28.1 fsevents: 2.3.3 jiti: 2.6.1 yaml: 2.8.1 - vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-istanbul@4.1.10)(vite@8.1.5(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.8.1)): + vitest@3.2.7(@types/node@24.13.3)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.1): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.7 + '@vitest/mocker': 3.2.7(vite@7.3.6(@types/node@24.13.3)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.1)) + '@vitest/pretty-format': 3.2.7 + '@vitest/runner': 3.2.7 + '@vitest/snapshot': 3.2.7 + '@vitest/spy': 3.2.7 + '@vitest/utils': 3.2.7 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.4.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.5 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.17 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.3.6(@types/node@24.13.3)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@24.13.3)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.13.3 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-istanbul@4.1.10)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.8.1)): dependencies: '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.8.1)) + '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.8.1)) '@vitest/pretty-format': 4.1.10 '@vitest/runner': 4.1.10 '@vitest/snapshot': 4.1.10 @@ -12669,7 +13471,7 @@ snapshots: tinyexec: 1.1.2 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.1.5(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.8.1) + vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.1