diff --git a/.github/workflows/code-quality.yaml b/.github/workflows/code-quality.yaml index 42bd1434..2c6fc152 100644 --- a/.github/workflows/code-quality.yaml +++ b/.github/workflows/code-quality.yaml @@ -14,14 +14,14 @@ jobs: runs-on: ubuntu-latest steps: - name: checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Node.js - uses: actions/setup-node@v2 + uses: actions/setup-node@v4 with: - node-version: '21' + node-version: '22' - name: Configure CI Git User run: | @@ -36,7 +36,7 @@ jobs: - name: Install pnpm run: | - npm i pnpm@latest -g + npm i pnpm@10.7.1 -g - name: Install dependencies run: pnpm install diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 9b9cb65b..db64837a 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Initialize CodeQL uses: github/codeql-action/init@v2 diff --git a/.github/workflows/publish-docker-image.yaml b/.github/workflows/publish-docker-image.yaml index 0d913c99..9f9a19c1 100644 --- a/.github/workflows/publish-docker-image.yaml +++ b/.github/workflows/publish-docker-image.yaml @@ -12,56 +12,49 @@ jobs: runs-on: ubuntu-latest steps: - name: checkout - uses: actions/checkout@v3 - - - name: Checkout and pull branch - run: | - LATEST_TAG=$(git describe --tags `git rev-list --tags --max-count=1`) - git checkout $LATEST_TAG + uses: actions/checkout@v4 - name: Setup buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v1 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERNAME }} password: ${{ secrets.DOCKERTOKEN }} - - name: Docker meta for core - id: meta_core - uses: docker/metadata-action@v3 + - name: Docker meta for API + id: meta_api + uses: docker/metadata-action@v5 with: images: codelit/medialit - name: Build and push API id: docker_build_api - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v6 with: context: . file: ./apps/api/Dockerfile push: true - tags: ${{ steps.meta_core.outputs.tags }} + tags: ${{ steps.meta_api.outputs.tags }} - name: Image digest run: echo ${{ steps.docker_build_api.outputs.digest }} - - name: Setup AWS ECR Details - uses: aws-actions/configure-aws-credentials@v4 + - name: Docker meta for Web app + id: meta_web + uses: docker/metadata-action@v5 with: - aws-access-key-id: ${{ secrets.AWS_ECR_ACCESS_KEY }} - aws-secret-access-key: ${{ secrets.AWS_ECR_ACCESS_SECRET }} - aws-region: ${{ secrets.AWS_REGION }} + images: codelit/medialit-web - - name: Login to AWS ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@v2 + - name: Build and push Web App + id: docker_build_web + uses: docker/build-push-action@v6 + with: + context: . + file: ./apps/web/Dockerfile + push: true + tags: ${{ steps.meta_web.outputs.tags }} - - name: Build, tag, and push docker images to Amazon ECR - env: - REGISTRY: ${{ steps.login-ecr.outputs.registry }} - REPOSITORY: ${{ secrets.AWS_ECR_REPO }} - IMAGE_TAG: latest - run: | - docker build -f apps/web/Dockerfile -t $REGISTRY/$REPOSITORY:$IMAGE_TAG . - docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG \ No newline at end of file + - name: Image digest + run: echo ${{ steps.docker_build_web.outputs.digest }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3ea94369..7fb78b64 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,8 @@ dist # IDE files .rgignore + +# Agent skills (clone manually from https://github.com/addyosmani/agent-skills) +.agent-skills/ +.claude/ +.agents/ diff --git a/.migrations/00006-mark-first-apikeys-default.js b/.migrations/00006-mark-first-apikeys-default.js new file mode 100644 index 00000000..05406192 --- /dev/null +++ b/.migrations/00006-mark-first-apikeys-default.js @@ -0,0 +1,28 @@ +// Find all unique userIds that have non-deleted API keys +const userIds = db.apikeys.distinct("userId", { deleted: { $ne: true } }); + +userIds.forEach((userId) => { + // Check if the user already has a default API key + const hasDefault = db.apikeys.findOne({ + userId: userId, + default: true, + deleted: { $ne: true }, + }); + if (hasDefault) { + return; // Already has a default key, skip + } + + // Find the oldest active API key for this user + const oldestKey = db.apikeys + .find({ userId: userId, deleted: { $ne: true } }) + .sort({ createdAt: 1 }) + .limit(1) + .toArray()[0]; + + if (oldestKey) { + db.apikeys.updateOne( + { _id: oldestKey._id }, + { $set: { default: true } }, + ); + } +}); diff --git a/AGENTS.md b/AGENTS.md index 95adfbb1..710e7f60 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,4 +1,4 @@ -# Dev Environment Tips +## Dev Environment -- Use `pnpm` as the package manager. -- This is a monorepo, so use `pnpm --filter ` to run commands in specific packages. +- Use `pnpm` as the package manager +- This is a monorepo — use `pnpm --filter ` for specific packages diff --git a/README.md b/README.md index 96555c08..15493388 100644 --- a/README.md +++ b/README.md @@ -1,84 +1,66 @@ # Introduction -MediaLit is a Node.js based app to store, convert and optimise the media files on any AWS S3 compatible storage. +MediaLit is a platform for uploading, transforming, and storing files on any S3-compatible storage provider. -## Setting up correct access on AWS S3 bucket +Use it as cloud storage for your apps, a personal media drive, or a file system for AI agents. MediaLit provides both a REST API and an MCP server for managing files programmatically. -Before you start uploading to your bucket, make sure you have set up the correct access on your S3 bucket. +## Managing your files -### 1. Without Cloudfront +This repository contains: -#### 1. Allow public access +- The backend API (under `apps/api`) +- The frontend (under `apps/web`) -![BLock public access](./apps/api/assets/without-cloudfront.png) +### Starting the API -#### 2. Allow ACLs +In order to upload files to the platform, you need to have an app. You can interact with the service using the app's API key. -### 2. With Cloudfront - -![BLock public access](./apps/api/assets/with-cloudfront.png) - -## Using Cloudfront - -If you need to use a Cloudfront CDN, you can enable it in the app, by setting up the following values in your .env file. +To create one, set up the following variable in your `.env` file: ```sh -ACCESS_PRIVATE_BUCKET_VIA_CLOUDFRONT=true -CDN_ENDPOINT=CLOUDFRONT_DISTRIBUTION_NAME -CLOUDFRONT_PRIVATE_KEY="PRIVATE_KEY" -CLOUDFRONT_KEY_PAIR_ID=KEY_PAIR_ID +EMAIL=email@yourdomain.com ``` -We assume that since you are using Cloudfront, you have locked down your bucket from public access. Therefore, all the files uploaded to the bucket will have ACL set to `private` i.e. they will require signed URLs in order to access them. +Then, start the API: -### Generating a key pair - -Use the following commands to generate a key pair to be used above. - -```sh -openssl genrsa -out private_key.pem 2048 -openssl rsa -pubout -in private_key.pem -out public_key.pem +```bash +pnpm --filter @medialit/api dev ``` -## Enable trust proxy +When the API starts for the very first time, a user with the provided email will be generated, and their subscription will be renewed for 10 years. -This app is based on [Express](https://expressjs.com/) which cannot work reliably when it is behind a proxy. For example, it cannot detect if it behind a proxy. +Additionally, a default app will be generated for the user and its API key will be printed in the application logs. The log containing the API key will look something like the following: -Hence, we need to enable it on our own. To do that, set the following environment variable. - -``` -ENABLE_TRUST_PROXY=true +```sh +{"level":30,"time":1781683124417,"pid":20848,"hostname":"hostname","apiKey":"kwtwsoMX3Xs_sDNxklMfz","msg":"Admin user created"} ``` -## Creating a User +> CAUTION: Keep the generated API key confidential, as anyone could use it to store files on your instance. -In order to interact with the service, you need to have a user. You can interact with the service using the user's API key. +### Starting the frontend -To create one, set up the following variable in your `.env` file: +The frontend is optional if you simply want to store, transform, and manage your files. -```sh -EMAIL=email@yourdomain.com -``` +Use the frontend if you want to: -When the app starts for the very first time, a user with the provided email will be generated, and their subscription will be renewed for 10 years. +- Manage files through a user interface +- Organize your files across multiple apps instead of putting everything in the default app -Additionally, an API key will be generated for the user and printed in the application logs. The log containing the API key will look something like the following: +To start the frontend: ```sh -@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ -@ API key: testcktI8Sa71QUgYtest @ -@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ +pnpm --filter @medialit/web dev ``` -> CAUTION: Keep the generated API key confidential, as anyone could use it to store files on your instance. +Then log in using the same email you provided above while booting up the API. ## API documentation -In order to interact with the service, you have to use the REST API. Our official Postman collection is available [here](https://www.postman.com/dark-rocket-625879/codelit/collection/5b8hfkr/medialit). +To interact with the service, you can use the REST API. Our API is documented [here](https://docs.medialit.cloud/api/createUploadSignature). ## Development -We build on Linux based systems. Hence the instructions are for those system only. If you are on Windows, we recommend using WSL. +We build on Linux-based systems. Hence, these instructions are for those systems only. If you are on Windows, we recommend using WSL. ### Install the utilities @@ -95,8 +77,7 @@ pnpm install ### Build packages ```bash -pnpm --filter=@medialit/images build -pnpm --filter=@medialit/thumbnail build +pnpm -r build ``` ### Run the service diff --git a/apps/api/.env.example b/apps/api/.env.example new file mode 100644 index 00000000..4874ca3a --- /dev/null +++ b/apps/api/.env.example @@ -0,0 +1,11 @@ +# --- OAuth 2.0 (Phase 3.5 — restart-safety hardening) --- +# Signing key for access + refresh tokens. MUST be at least 32 bytes. +# Generate with: openssl rand -base64 48 +# +# For key rotation, set a comma-separated list — the first key signs +# new tokens, all listed keys are accepted for verification. +OAUTH_SIGNING_KEY= + +# Access token lifetime in seconds. Keep short; refresh tokens are used to +# continue sessions and are revoked on logout. +TOKEN_TTL_SECONDS=900 diff --git a/apps/api/__tests__/mcp/media-tool.test.ts b/apps/api/__tests__/mcp/media-tool.test.ts new file mode 100644 index 00000000..cf54313f --- /dev/null +++ b/apps/api/__tests__/mcp/media-tool.test.ts @@ -0,0 +1,42 @@ +import { Constants } from "@medialit/models"; +import test, { afterEach, describe, mock } from "node:test"; +import assert from "node:assert"; +import { handleGetTotalStorageTool } from "../../src/mcp/tools/media"; +import { maxStorageAllowedSubscribed } from "../../src/config/constants"; + +describe("MCP get_total_storage", () => { + afterEach(() => { + mock.restoreAll(); + }); + + test("uses the authenticated user id and returns the account storage limit", async () => { + const user = { + id: "string-user-id", + _id: "object-user-id", + subscriptionStatus: Constants.SubscriptionStatus.SUBSCRIBED, + }; + + let queriedUserId: unknown; + const response = await handleGetTotalStorageTool( + { + authInfo: { + clientId: user.id, + token: "test-api-key", + user, + }, + }, + { + getTotalSpace: async ({ userId }: any) => { + queriedUserId = userId; + return 2103931; + }, + }, + ); + + assert.equal(queriedUserId, user._id); + assert.deepEqual((response as any).structuredContent, { + storage: 2103931, + maxStorage: maxStorageAllowedSubscribed, + }); + }); +}); diff --git a/apps/api/__tests__/mcp/upload-tool.test.ts b/apps/api/__tests__/mcp/upload-tool.test.ts new file mode 100644 index 00000000..e174d067 --- /dev/null +++ b/apps/api/__tests__/mcp/upload-tool.test.ts @@ -0,0 +1,82 @@ +import { Constants } from "@medialit/models"; +import test, { afterEach, describe, mock } from "node:test"; +import assert from "node:assert"; +import { handleUploadMediaTool } from "../../src/mcp/tools/upload"; +import mediaQueries from "../../src/media/queries"; +import mediaService from "../../src/media/service"; +import { + maxFileUploadSizeNotSubscribed, + maxStorageAllowedNotSubscribed, +} from "../../src/config/constants"; +import { + FILE_SIZE_EXCEEDED, + NOT_ENOUGH_STORAGE, +} from "../../src/config/strings"; + +function authInfo(user: any) { + return { + authInfo: { + clientId: user.id, + token: "test-api-key", + user, + }, + }; +} + +function uploadArgs(size: number) { + return { + fileBase64: Buffer.alloc(size).toString("base64"), + fileName: "test.txt", + mimeType: "text/plain", + }; +} + +describe("MCP upload_media", () => { + afterEach(() => { + mock.restoreAll(); + }); + + test("rejects uploads that exceed the account file size limit", async () => { + const user = { + id: "test-user-id", + subscriptionStatus: Constants.SubscriptionStatus.NOT_SUBSCRIBED, + }; + + mock.method(mediaService, "upload").mock.mockImplementation(() => { + throw new Error("upload should not be called"); + }); + + const response = await handleUploadMediaTool( + uploadArgs(maxFileUploadSizeNotSubscribed + 1), + authInfo(user), + ); + + assert.equal((response as any).isError, true); + assert.equal( + response.content[0].text, + `${FILE_SIZE_EXCEEDED}. Allowed: ${maxFileUploadSizeNotSubscribed} bytes`, + ); + }); + + test("rejects uploads that exceed remaining account storage", async () => { + const user = { + id: "test-user-id", + subscriptionStatus: Constants.SubscriptionStatus.NOT_SUBSCRIBED, + }; + + mock.method(mediaQueries, "getTotalSpace").mock.mockImplementation( + async () => maxStorageAllowedNotSubscribed, + ); + mock.method(mediaService, "upload").mock.mockImplementation(() => { + throw new Error("upload should not be called"); + }); + + const response = await handleUploadMediaTool( + uploadArgs(1024), + authInfo(user), + ); + + assert.equal((response as any).isError, true); + assert.equal(response.content[0].text, NOT_ENOUGH_STORAGE); + }); +}); diff --git a/apps/api/__tests__/media/handlers.test.ts b/apps/api/__tests__/media/handlers.test.ts index 093fd920..f59290ec 100644 --- a/apps/api/__tests__/media/handlers.test.ts +++ b/apps/api/__tests__/media/handlers.test.ts @@ -2,7 +2,6 @@ import { Constants } from "@medialit/models"; import test, { afterEach, describe, mock } from "node:test"; import { uploadMedia } from "../../src/media/handlers"; import assert from "node:assert"; -import { FILE_SIZE_EXCEEDED } from "../../src/config/strings"; import mediaService from "../../src/media/service"; describe("Media handlers", () => { @@ -10,60 +9,6 @@ describe("Media handlers", () => { mock.restoreAll(); }); - test("should reject upload if file size exceeds limit for non-subscribed user", async () => { - const req = { - files: { - file: { - size: 100000000, // 100MB - }, - }, - user: { - id: "123", - subscriptionStatus: Constants.SubscriptionStatus.NOT_SUBSCRIBED, - }, - socket: { - setTimeout: () => {}, - }, - }; - - const res = { - status: (code: number) => ({ - json: (data: any) => ({ code, data }), - }), - }; - - const response = await uploadMedia(req, res, () => {}); - assert.strictEqual(response.code, 400); - assert.ok(response.data.error.includes(FILE_SIZE_EXCEEDED)); - }); - - test("should reject upload if file size exceeds limit for subscribed user", async () => { - const req = { - files: { - file: { - size: 2147483648 + 1, // 2GB + 1 byte - }, - }, - user: { - id: "123", - subscriptionStatus: Constants.SubscriptionStatus.SUBSCRIBED, - }, - socket: { - setTimeout: () => {}, - }, - }; - - const res = { - status: (code: number) => ({ - json: (data: any) => ({ code, data }), - }), - }; - - const response = await uploadMedia(req, res, () => {}); - assert.strictEqual(response.code, 400); - assert.ok(response.data.error.includes(FILE_SIZE_EXCEEDED)); - }); - test("should allow larger file upload for subscribed user", async () => { const req = { files: { diff --git a/apps/api/__tests__/media/storage-middleware.test.ts b/apps/api/__tests__/media/storage-middleware.test.ts index 27277eb1..698a8a29 100644 --- a/apps/api/__tests__/media/storage-middleware.test.ts +++ b/apps/api/__tests__/media/storage-middleware.test.ts @@ -4,10 +4,14 @@ import assert from "node:assert"; import storageValidation from "../../src/media/storage-middleware"; import mediaQueries from "../../src/media/queries"; import { + maxFileUploadSizeNotSubscribed, maxStorageAllowedNotSubscribed, maxStorageAllowedSubscribed, } from "../../src/config/constants"; -import { NOT_ENOUGH_STORAGE } from "../../src/config/strings"; +import { + FILE_SIZE_EXCEEDED, + NOT_ENOUGH_STORAGE, +} from "../../src/config/strings"; describe("storageValidation middleware", () => { afterEach(() => { @@ -114,6 +118,39 @@ describe("storageValidation middleware", () => { assert.strictEqual(nextCalled, false); }); + test("should reject upload when file exceeds upload size limit", async () => { + const req = { + user: { + id: "test-user-id", + subscriptionStatus: Constants.SubscriptionStatus.NOT_SUBSCRIBED, + }, + files: { + file: { + size: maxFileUploadSizeNotSubscribed + 1, + }, + }, + }; + + const res = { + status: (code: number) => ({ + json: (data: any) => ({ code, data }), + }), + }; + + let nextCalled = false; + const next = () => { + nextCalled = true; + }; + + const response = await storageValidation(req, res, next); + assert.strictEqual(response.code, 400); + assert.strictEqual( + response.data.error, + `${FILE_SIZE_EXCEEDED}. Allowed: ${maxFileUploadSizeNotSubscribed} bytes`, + ); + assert.strictEqual(nextCalled, false); + }); + test("should reject upload when user exceeds storage limit (subscribed)", async () => { const req = { user: { diff --git a/apps/api/__tests__/tus/tus-server.test.ts b/apps/api/__tests__/tus/tus-server.test.ts new file mode 100644 index 00000000..5b7c9dc3 --- /dev/null +++ b/apps/api/__tests__/tus/tus-server.test.ts @@ -0,0 +1,81 @@ +import { Constants } from "@medialit/models"; +import test, { afterEach, describe, mock } from "node:test"; +import assert from "node:assert"; +import { handleTusUploadCreate } from "../../src/tus/tus-server"; +import mediaQueries from "../../src/media/queries"; +import { createTusUpload } from "../../src/tus/queries"; +import { + maxFileUploadSizeNotSubscribed, + maxStorageAllowedNotSubscribed, +} from "../../src/config/constants"; +import { + FILE_SIZE_EXCEEDED, + NOT_ENOUGH_STORAGE, +} from "../../src/config/strings"; + +function req(user: any) { + return { + user, + apikey: "test-api-key", + group: "default", + }; +} + +function upload(size: number) { + return { + id: "upload-id", + size, + metadata: { + fileName: "test.txt", + mimeType: "text/plain", + }, + }; +} + +describe("TUS upload creation", () => { + afterEach(() => { + mock.restoreAll(); + }); + + test("rejects uploads that exceed the account file size limit", async () => { + const user = { + id: "test-user-id", + subscriptionStatus: Constants.SubscriptionStatus.NOT_SUBSCRIBED, + }; + + mock.fn(createTusUpload).mock.mockImplementation(() => { + throw new Error("createTusUpload should not be called"); + }); + + await assert.rejects( + () => + handleTusUploadCreate( + req(user), + upload(maxFileUploadSizeNotSubscribed + 1), + ), + { + status_code: 403, + body: `${FILE_SIZE_EXCEEDED}. Allowed: ${maxFileUploadSizeNotSubscribed} bytes`, + }, + ); + }); + + test("rejects uploads that exceed remaining account storage", async () => { + const user = { + id: "test-user-id", + subscriptionStatus: Constants.SubscriptionStatus.NOT_SUBSCRIBED, + }; + + mock.method(mediaQueries, "getTotalSpace").mock.mockImplementation( + async () => maxStorageAllowedNotSubscribed, + ); + + await assert.rejects( + () => handleTusUploadCreate(req(user), upload(1024)), + { + status_code: 403, + body: NOT_ENOUGH_STORAGE, + }, + ); + }); +}); diff --git a/apps/api/package.json b/apps/api/package.json index c13f98a6..b405a4d3 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -40,6 +40,8 @@ "@medialit/models": "workspace:*", "@medialit/thumbnail": "workspace:*", "@medialit/utils": "workspace:^0.1.0", + "@modelcontextprotocol/sdk": "^1.29.0", + "@node-oauth/oauth2-server": "^5.3.0", "@tus/file-store": "^2.0.0", "@tus/server": "^2.3.0", "aws-sdk": "^2.1692.0", @@ -47,21 +49,26 @@ "dotenv": "^17.2.3", "express": "^4.2.0", "express-fileupload": "^1.5.2", + "express-rate-limit": "^7.5.0", "joi": "^17.6.0", "joi-to-swagger": "^6.2.0", + "jsonwebtoken": "^9.0.2", "mongoose": "^8.19.3", "passport": "^0.7.0", "passport-jwt": "^4.0.1", "pino": "^10.1.0", - "swagger-ui-express": "^5.0.1" + "swagger-ui-express": "^5.0.1", + "nodemailer": "^6.10.0", + "zod": "^3.25.76" }, "devDependencies": { "@types/cors": "^2.8.12", "@types/express": "^4.17.20", "@types/express-fileupload": "^1.2.2", "@types/joi": "^17.2.3", - "@types/mongoose": "^5.11.97", + "@types/jsonwebtoken": "^9.0.10", "@types/node": "^22.14.1", + "@types/nodemailer": "^6.4.17", "@types/passport": "^1.0.7", "@types/passport-jwt": "^3.0.6", "@types/swagger-ui-express": "^4.1.8", @@ -73,4 +80,4 @@ "tsx": "^4.20.6", "typescript": "^5.9.3" } -} +} \ No newline at end of file diff --git a/apps/api/src/apikey/middleware.ts b/apps/api/src/apikey/middleware.ts index d0ebac8b..3a85eca8 100644 --- a/apps/api/src/apikey/middleware.ts +++ b/apps/api/src/apikey/middleware.ts @@ -1,28 +1 @@ -import { BAD_REQUEST, UNAUTHORISED } from "../config/strings"; -import { getApiKeyUsingKeyId } from "./queries"; -import { getUser } from "../user/queries"; -import { Apikey } from "@medialit/models"; -import logger from "../services/log"; - -export default async function apikey( - req: any, - res: any, - next: (...args: any[]) => void, -) { - const reqKey = req.body?.apikey || req.headers["x-medialit-apikey"]; - - if (!reqKey) { - logger.error({}, "API key is missing"); - return res.status(401).json({ error: UNAUTHORISED }); - } - - const apiKey: Apikey | null = await getApiKeyUsingKeyId(reqKey); - if (!apiKey) { - return res.status(401).json({ error: UNAUTHORISED }); - } - - req.user = await getUser(apiKey!.userId.toString()); - req.apikey = apiKey.key; - - next(); -} +export { apikey as default } from "../auth/middleware"; diff --git a/apps/api/src/apikey/queries.ts b/apps/api/src/apikey/queries.ts index 8b0d9355..b3bca1f0 100644 --- a/apps/api/src/apikey/queries.ts +++ b/apps/api/src/apikey/queries.ts @@ -5,11 +5,13 @@ import { getUniqueId } from "@medialit/utils"; export async function createApiKey( userId: string, name: string, + isDefault: boolean = false, ): Promise { return await ApikeyModel.create({ name, key: getUniqueId(), userId, + default: isDefault, }); } @@ -28,6 +30,7 @@ export async function getApiKeyByUserId( key: 1, httpReferrers: 1, ipAddresses: 1, + default: 1, createdAt: 1, updatedAt: 1, }; @@ -46,19 +49,8 @@ export async function getApiKeyByUserId( return result; } -// export async function deleteApiKey( -// userId: string, -// keyId: string, -// ): Promise { -// await ApikeyModel.deleteOne({ -// key: keyId, -// userId, -// }); -// } - export default { createApiKey, getApiKeyUsingKeyId, getApiKeyByUserId, - // deleteApiKey, }; diff --git a/apps/api/src/auth/__tests__/middleware.test.ts b/apps/api/src/auth/__tests__/middleware.test.ts new file mode 100644 index 00000000..e313b3e0 --- /dev/null +++ b/apps/api/src/auth/__tests__/middleware.test.ts @@ -0,0 +1,159 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { createAuthMiddleware } from "../middleware.js"; +import { AuthResult } from "../resolve-auth.js"; + +function response() { + return { + statusCode: 200, + body: undefined as unknown, + status(code: number) { + this.statusCode = code; + return this; + }, + json(body: unknown) { + this.body = body; + return this; + }, + }; +} + +test("REST middleware maps successful OAuth auth to req.user and req.apikey", async () => { + const req: any = { headers: { authorization: "Bearer token" }, body: {} }; + const res = response(); + let nextCalled = false; + const middleware = createAuthMiddleware("rest", async () => ({ + status: "authenticated", + kind: "oauth", + user: { id: "user-1" }, + userId: "user-1", + clientId: "client-1", + scopes: ["read"], + apiKey: "default-key", + })); + + await middleware(req, res as any, () => { + nextCalled = true; + }); + + assert.equal(nextCalled, true); + assert.deepEqual(req.user, { id: "user-1" }); + assert.equal(req.apikey, "default-key"); +}); + +test("REST middleware maps API-key auth to the submitted API key", async () => { + const req: any = { + headers: { "x-medialit-apikey": "submitted-key" }, + body: {}, + }; + const res = response(); + let nextCalled = false; + const middleware = createAuthMiddleware("rest", async () => ({ + status: "authenticated", + kind: "apikey", + user: { id: "user-1" }, + userId: "user-1", + apiKey: "submitted-key", + })); + + await middleware(req, res as any, () => { + nextCalled = true; + }); + + assert.equal(nextCalled, true); + assert.deepEqual(req.user, { id: "user-1" }); + assert.equal(req.apikey, "submitted-key"); +}); + +test("REST middleware uses shared invalid Bearer 401 behavior", async () => { + const req: any = { headers: { authorization: "Bearer bad" }, body: {} }; + const res = response(); + const middleware = createAuthMiddleware("rest", async () => ({ + status: "invalid_token", + })); + + await middleware(req, res as any, () => { + throw new Error("next should not be called"); + }); + + assert.equal(res.statusCode, 401); + assert.deepEqual(res.body, { + error: "invalid_token", + error_description: "Access token is invalid or expired", + }); +}); + +test("MCP middleware maps OAuth auth fields", async () => { + const req: any = { headers: { authorization: "Bearer token" }, body: {} }; + const res = response(); + let nextCalled = false; + const middleware = createAuthMiddleware("mcp", async () => ({ + status: "authenticated", + kind: "oauth", + user: { id: "user-1" }, + userId: "user-1", + clientId: "client-1", + scopes: ["read"], + apiKey: "default-key", + })); + + await middleware(req, res as any, () => { + nextCalled = true; + }); + + assert.equal(nextCalled, true); + assert.deepEqual(req.user, { id: "user-1" }); + assert.equal(req.userId, "user-1"); + assert.equal(req.clientId, "client-1"); + assert.deepEqual(req.scopes, ["read"]); + assert.equal(req.apikey, "default-key"); +}); + +test("MCP middleware maps API-key auth fields without OAuth fields", async () => { + const req: any = { + headers: { "x-medialit-apikey": "submitted-key" }, + body: {}, + }; + const res = response(); + let nextCalled = false; + const middleware = createAuthMiddleware("mcp", async () => ({ + status: "authenticated", + kind: "apikey", + user: { id: "user-1" }, + userId: "user-1", + apiKey: "submitted-key", + })); + + await middleware(req, res as any, () => { + nextCalled = true; + }); + + assert.equal(nextCalled, true); + assert.deepEqual(req.user, { id: "user-1" }); + assert.equal(req.userId, "user-1"); + assert.equal(req.apikey, "submitted-key"); + assert.equal(req.clientId, undefined); + assert.equal(req.scopes, undefined); +}); + +test("MCP middleware preserves missing-auth 401 behavior", async () => { + const req: any = { headers: {}, body: {} }; + const res = response(); + const middleware = createAuthMiddleware( + "mcp", + async (): Promise => ({ + status: "missing", + }), + ); + + await middleware(req, res as any, () => { + throw new Error("next should not be called"); + }); + + assert.equal(res.statusCode, 401); + assert.deepEqual(res.body, { + error: "unauthorized", + error_description: + "Missing authentication: provide Authorization: Bearer or x-medialit-apikey header", + }); +}); diff --git a/apps/api/src/auth/__tests__/resolve-auth.test.ts b/apps/api/src/auth/__tests__/resolve-auth.test.ts new file mode 100644 index 00000000..c60f04ae --- /dev/null +++ b/apps/api/src/auth/__tests__/resolve-auth.test.ts @@ -0,0 +1,133 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { resolveAuth, selectEffectiveApiKey } from "../resolve-auth.js"; +import { Apikey } from "@medialit/models"; + +const user = { _id: "user-1", id: "user-1" }; + +function apiKey(key: string, overrides: Partial = {}): Apikey { + return { + key, + keyId: key, + name: key, + userId: { toString: () => "user-1" } as any, + default: false, + deleted: false, + ...overrides, + }; +} + +function dependencies(overrides: Partial[1]>) { + return { + validateBearerToken: async () => ({ + userId: "user-1", + clientId: "client-1", + scopes: ["read"], + }), + getUser: async () => user, + getApiKeyByUserId: async () => [apiKey("first")], + getApiKeyUsingKeyId: async (key: string) => apiKey(key), + ...overrides, + } as NonNullable[1]>; +} + +test("selectEffectiveApiKey prefers the default key", () => { + const selected = selectEffectiveApiKey([ + apiKey("first"), + apiKey("default", { default: true }), + ]); + + assert.equal(selected?.key, "default"); +}); + +test("selectEffectiveApiKey falls back to the first key", () => { + const selected = selectEffectiveApiKey([apiKey("first"), apiKey("second")]); + + assert.equal(selected?.key, "first"); +}); + +test("OAuth auth resolves the user's default API key", async () => { + const auth = await resolveAuth( + { authorization: "Bearer valid-token" }, + dependencies({ + getApiKeyByUserId: async () => [ + apiKey("first"), + apiKey("default", { default: true }), + ], + }), + ); + + assert.equal(auth.status, "authenticated"); + assert.equal(auth.kind, "oauth"); + assert.equal(auth.apiKey, "default"); +}); + +test("OAuth auth falls back to the first API key when no default exists", async () => { + const auth = await resolveAuth( + { authorization: "Bearer valid-token" }, + dependencies({ + getApiKeyByUserId: async () => [apiKey("first"), apiKey("second")], + }), + ); + + assert.equal(auth.status, "authenticated"); + assert.equal(auth.kind, "oauth"); + assert.equal(auth.apiKey, "first"); +}); + +test("OAuth auth succeeds without an API key when the user has none", async () => { + const auth = await resolveAuth( + { authorization: "Bearer valid-token" }, + dependencies({ getApiKeyByUserId: async () => [] }), + ); + + assert.equal(auth.status, "authenticated"); + assert.equal(auth.kind, "oauth"); + assert.equal(auth.apiKey, undefined); +}); + +test("invalid Bearer token is rejected before API-key fallback", async () => { + const auth = await resolveAuth( + { + authorization: "Bearer invalid-token", + apiKeyHeader: "submitted-key", + }, + dependencies({ + validateBearerToken: async () => null, + getApiKeyUsingKeyId: async () => { + throw new Error("API key fallback must not run"); + }, + }), + ); + + assert.deepEqual(auth, { status: "invalid_token" }); +}); + +test("OAuth auth rejects a token for a missing user", async () => { + const auth = await resolveAuth( + { authorization: "Bearer valid-token" }, + dependencies({ getUser: async () => null }), + ); + + assert.deepEqual(auth, { status: "unauthorized" }); +}); + +test("API-key auth preserves the submitted key", async () => { + const auth = await resolveAuth( + { apiKeyHeader: "submitted-key" }, + dependencies({ + getApiKeyUsingKeyId: async () => + apiKey("stored-key", { default: true }), + }), + ); + + assert.equal(auth.status, "authenticated"); + assert.equal(auth.kind, "apikey"); + assert.equal(auth.apiKey, "submitted-key"); +}); + +test("missing auth returns missing", async () => { + const auth = await resolveAuth({}, dependencies({})); + + assert.deepEqual(auth, { status: "missing" }); +}); diff --git a/apps/api/src/auth/limiters.ts b/apps/api/src/auth/limiters.ts new file mode 100644 index 00000000..ff4030e3 --- /dev/null +++ b/apps/api/src/auth/limiters.ts @@ -0,0 +1,12 @@ +import rateLimit from "express-rate-limit"; + +export const authenticatedApiLimiter = rateLimit({ + windowMs: 60_000, + max: 120, + standardHeaders: true, + legacyHeaders: false, + message: { + error: "too_many_requests", + error_description: "Too many requests.", + }, +}); diff --git a/apps/api/src/auth/middleware.ts b/apps/api/src/auth/middleware.ts new file mode 100644 index 00000000..7553b3c2 --- /dev/null +++ b/apps/api/src/auth/middleware.ts @@ -0,0 +1,55 @@ +import { NextFunction, Response } from "express"; +import { AuthResult, resolveAuth, sendAuthError } from "./resolve-auth"; + +type AuthResolver = (input: { + authorization?: unknown; + apiKeyHeader?: unknown; + bodyApiKey?: unknown; +}) => Promise; + +type AuthMiddlewareMode = "rest" | "mcp"; + +function applyAuthToRequest( + req: any, + auth: AuthResult, + mode: AuthMiddlewareMode, +) { + if (auth.status !== "authenticated") return; + + req.user = auth.user; + req.apikey = auth.apiKey; + + if (mode === "mcp") { + req.userId = auth.userId; + if (auth.kind === "oauth") { + req.clientId = auth.clientId; + req.scopes = auth.scopes; + } + } +} + +export function createAuthMiddleware( + mode: AuthMiddlewareMode, + authResolver: AuthResolver = resolveAuth, +) { + return async function authMiddleware( + req: any, + res: Response, + next: NextFunction, + ): Promise { + const auth = await authResolver({ + authorization: req.headers.authorization, + apiKeyHeader: req.headers["x-medialit-apikey"], + bodyApiKey: req.body?.apikey, + }); + + if (sendAuthError(res, auth)) return; + if (auth.status !== "authenticated") return; + + applyAuthToRequest(req, auth, mode); + next(); + }; +} + +export const apikey = createAuthMiddleware("rest"); +export const mcpAuth = createAuthMiddleware("mcp"); diff --git a/apps/api/src/auth/resolve-auth.ts b/apps/api/src/auth/resolve-auth.ts new file mode 100644 index 00000000..dc79bcb8 --- /dev/null +++ b/apps/api/src/auth/resolve-auth.ts @@ -0,0 +1,155 @@ +import { Apikey } from "@medialit/models"; +import { getApiKeyByUserId, getApiKeyUsingKeyId } from "../apikey/queries"; +import { validateBearerToken } from "../oauth/middleware"; +import { getUser } from "../user/queries"; + +type UserRecord = any; + +type OAuthClaims = { + userId: string; + clientId: string; + scopes: string[]; +}; + +export type AuthInput = { + authorization?: unknown; + apiKeyHeader?: unknown; + bodyApiKey?: unknown; +}; + +export type AuthDependencies = { + validateBearerToken: (bearer: string) => Promise; + getUser: (id: string) => Promise; + getApiKeyByUserId: (userId: string) => Promise; + getApiKeyUsingKeyId: (key: string) => Promise; +}; + +export type AuthResult = + | { + status: "authenticated"; + kind: "oauth"; + user: UserRecord; + userId: string; + clientId: string; + scopes: string[]; + apiKey?: string; + } + | { + status: "authenticated"; + kind: "apikey"; + user: UserRecord; + userId: string; + apiKey: string; + } + | { status: "invalid_token" } + | { status: "unauthorized" } + | { status: "missing" }; + +export function sendAuthError(res: any, auth: AuthResult): boolean { + if (auth.status === "invalid_token") { + res.status(401).json({ + error: "invalid_token", + error_description: "Access token is invalid or expired", + }); + return true; + } + + if (auth.status === "missing") { + res.status(401).json({ + error: "unauthorized", + error_description: + "Missing authentication: provide Authorization: Bearer or x-medialit-apikey header", + }); + return true; + } + + if (auth.status === "unauthorized") { + res.status(401).json({ error: "unauthorized" }); + return true; + } + + return false; +} + +const defaultDependencies: AuthDependencies = { + validateBearerToken, + getUser, + getApiKeyByUserId, + getApiKeyUsingKeyId, +}; + +function getHeaderValue(value: unknown): string | undefined { + if (Array.isArray(value)) { + return typeof value[0] === "string" ? value[0] : undefined; + } + return typeof value === "string" ? value : undefined; +} + +export function selectEffectiveApiKey( + keys: Apikey | Apikey[] | null, +): Apikey | null { + if (!keys) return null; + if (!Array.isArray(keys)) return keys; + return keys.find((key) => key.default === true) || keys[0] || null; +} + +async function getEffectiveOAuthApiKey( + userId: string, + dependencies: AuthDependencies, +): Promise { + try { + const keys = await dependencies.getApiKeyByUserId(userId); + return selectEffectiveApiKey(keys)?.key; + } catch { + return undefined; + } +} + +export async function resolveAuth( + input: AuthInput, + dependencies: AuthDependencies = defaultDependencies, +): Promise { + const authorization = getHeaderValue(input.authorization); + if (authorization) { + const match = authorization.match(/^Bearer (.+)$/i); + if (match) { + const claims = await dependencies.validateBearerToken(match[1]); + if (!claims) return { status: "invalid_token" }; + + const user = await dependencies.getUser(claims.userId); + if (!user) return { status: "unauthorized" }; + + return { + status: "authenticated", + kind: "oauth", + user, + userId: claims.userId, + clientId: claims.clientId, + scopes: claims.scopes, + apiKey: await getEffectiveOAuthApiKey( + claims.userId, + dependencies, + ), + }; + } + } + + const submittedApiKey = + getHeaderValue(input.bodyApiKey) || getHeaderValue(input.apiKeyHeader); + if (!submittedApiKey) return { status: "missing" }; + + const apiKey = await dependencies.getApiKeyUsingKeyId(submittedApiKey); + if (!apiKey) return { status: "unauthorized" }; + + const userId = apiKey.userId.toString(); + const user = await dependencies.getUser(userId); + if (!user) return { status: "unauthorized" }; + + return { + status: "authenticated", + kind: "apikey", + user, + userId, + apiKey: submittedApiKey, + }; +} diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index b495544e..e53e4a32 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -8,10 +8,11 @@ import mediaRoutes from "./media/routes"; import signatureRoutes from "./signature/routes"; import mediaSettingsRoutes from "./media-settings/routes"; import tusRoutes from "./tus/routes"; +import mcpRoutes from "./mcp/routes"; import logger from "./services/log"; import { createUser, findByEmail } from "./user/queries"; import { Apikey, User } from "@medialit/models"; -import { createApiKey } from "./apikey/queries"; +import { getApiKeyByUserId } from "./apikey/queries"; import swaggerUi from "swagger-ui-express"; import swaggerOutput from "./swagger_output.json"; @@ -23,9 +24,10 @@ import { HOUR_IN_SECONDS } from "./config/constants"; connectToDatabase(); const app = express(); -app.set("trust proxy", process.env.ENABLE_TRUST_PROXY === "true"); +app.set("trust proxy", process.env.ENABLE_TRUST_PROXY === "true" ? 1 : false); app.use(express.json()); +app.use(express.urlencoded({ extended: false })); app.get( "/health", @@ -88,6 +90,7 @@ app.use("/settings/media", mediaSettingsRoutes(passport)); app.use("/media/signature", signatureRoutes); app.use("/media", tusRoutes); app.use("/media", mediaRoutes); +app.use(mcpRoutes); app.get( "/cleanup/temp", @@ -165,6 +168,15 @@ async function checkConfig() { "If CDN_ENDPOINT is not set, both CLOUD_ENDPOINT and CLOUD_ENDPOINT_PUBLIC must be provided", ); } + if ( + !process.env.OAUTH_SIGNING_KEY || + Buffer.byteLength(process.env.OAUTH_SIGNING_KEY, "utf8") < 32 + ) { + throw new Error( + "OAUTH_SIGNING_KEY is required and must be at least 32 bytes (256 bits). " + + "Generate one with: openssl rand -base64 48", + ); + } } async function checkDependencies() { @@ -203,10 +215,9 @@ async function createAdminUser() { if (!user) { const user = await createUser(email, undefined, "subscribed"); - const apikey: Apikey = await createApiKey(user.id, "App 1"); - console.log("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"); - console.log(`@ API key: ${apikey.key} @`); - console.log("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"); + const keys = await getApiKeyByUserId(user.id); + const firstKey = Array.isArray(keys) ? keys[0] : keys; + logger.info({ apiKey: firstKey?.key }, "Admin user created"); } } catch (error) { logger.error({ error }, "Failed to create admin user"); diff --git a/apps/api/src/mcp/routes.ts b/apps/api/src/mcp/routes.ts new file mode 100644 index 00000000..827ad494 --- /dev/null +++ b/apps/api/src/mcp/routes.ts @@ -0,0 +1,117 @@ +import { Router } from "express"; +import rateLimit from "express-rate-limit"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { mcpAuth } from "../auth/middleware"; +import { oauthRouter } from "../oauth/routes"; +import { createMCPSession } from "./server"; + +const router = Router(); + +const mcpLimiter = rateLimit({ + windowMs: 60_000, + max: 60, + standardHeaders: true, + legacyHeaders: false, + message: { + error: "too_many_requests", + error_description: "Too many requests.", + }, +}); + +const mcpSessions = new Map(); + +const mcpCors = (req: any, res: any, next: any) => { + const origin = req.headers.origin || "*"; + res.header("Access-Control-Allow-Origin", origin); + res.header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS"); + res.header( + "Access-Control-Allow-Headers", + "Content-Type, Accept, Mcp-Session-Id, Mcp-Protocol-Version, x-medialit-apikey, Authorization", + ); + res.header("Access-Control-Expose-Headers", "Mcp-Session-Id"); + if (req.method === "OPTIONS") { + return res.status(204).end(); + } + next(); +}; + +function patchMcpAcceptHeaders(req: any) { + const accept = req.headers.accept || ""; + const needsJson = !accept.includes("application/json"); + const needsSSE = !accept.includes("text/event-stream"); + if (!needsJson && !needsSSE) return; + + const additions: string[] = []; + if (needsJson) additions.push("application/json"); + if (needsSSE) additions.push("text/event-stream"); + const newAccept = accept + ? `${accept}, ${additions.join(", ")}` + : additions.join(", "); + req.headers.accept = newAccept; + + const rawHeaders: string[] = req.rawHeaders; + let found = false; + for (let i = 0; i < rawHeaders.length; i += 2) { + if (rawHeaders[i].toLowerCase() === "accept") { + rawHeaders[i + 1] = newAccept; + found = true; + break; + } + } + if (!found) rawHeaders.push("Accept", newAccept); +} + +function getMcpAuth(req: any) { + return { + token: req.apikey || "", + clientId: String(req.userId || req.user?._id || req.user?.id || ""), + user: req.user, + scopes: [] as string[], + }; +} + +router.use(["/.well-known", "/oauth"], mcpCors); +router.use(oauthRouter); + +router.post( + "/mcp", + mcpCors, + mcpLimiter, + mcpAuth, + async (req: any, res: any) => { + patchMcpAcceptHeaders(req); + + const auth = getMcpAuth(req); + const sessionId = req.headers["mcp-session-id"] as string | undefined; + + if (sessionId) { + const transport = mcpSessions.get(sessionId); + if (!transport) { + return res.status(404).json({ + jsonrpc: "2.0", + error: { code: -32001, message: "Session not found" }, + id: null, + }); + } + await transport.handleRequest( + Object.assign(req, { auth }), + res, + req.body, + ); + } else { + const transport = createMCPSession( + (id) => mcpSessions.set(id, transport), + (id) => mcpSessions.delete(id), + ); + await transport.handleRequest( + Object.assign(req, { auth }), + res, + req.body, + ); + } + }, +); + +router.options("/mcp", mcpCors); + +export default router; diff --git a/apps/api/src/mcp/server.ts b/apps/api/src/mcp/server.ts new file mode 100644 index 00000000..bf40f85d --- /dev/null +++ b/apps/api/src/mcp/server.ts @@ -0,0 +1,40 @@ +import crypto from "crypto"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { registerMediaTools } from "./tools/media"; +import { registerSignatureTool } from "./tools/signature"; +import { registerSettingsTools } from "./tools/settings"; +import { registerUploadTool } from "./tools/upload"; + +function registerAllTools(server: McpServer): void { + registerMediaTools(server); + registerSignatureTool(server); + registerSettingsTools(server); + registerUploadTool(server); +} + +/** + * Create a new MCP session (transport + server pair). + * Each connecting client must get its own session — the + * WebStandardStreamableHTTPServerTransport is single-session by design. + */ +export function createMCPSession( + onsessioninitialized: (sessionId: string) => void, + onsessionclosed: (sessionId: string) => void, +): StreamableHTTPServerTransport { + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => crypto.randomUUID(), + enableJsonResponse: true, + onsessioninitialized, + onsessionclosed, + }); + const server = new McpServer({ + name: "MediaLit", + version: "1.0.0", + description: + "MediaLit MCP server — manage media files, storage, and upload settings for a MediaLit account. Supports listing, inspecting, deleting, and sealing media items, querying storage usage, generating upload signatures, and configuring media processing settings.", + }); + registerAllTools(server); + server.connect(transport); + return transport; +} diff --git a/apps/api/src/mcp/tools/media.ts b/apps/api/src/mcp/tools/media.ts new file mode 100644 index 00000000..2f51af0f --- /dev/null +++ b/apps/api/src/mcp/tools/media.ts @@ -0,0 +1,300 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { + maxStorageAllowedNotSubscribed, + maxStorageAllowedSubscribed, +} from "../../config/constants"; +import mediaService from "../../media/service"; +import * as mediaQueries from "../../media/queries"; +import { getSubscriptionStatus } from "@medialit/models"; +import { NOT_FOUND, SUCCESS } from "../../config/strings"; +import { AUTH_ERROR, INTERNAL_ERROR } from "./responses"; +import { + mediaListSchema, + mediaSchema, + storageSchema, + successMessageSchema, +} from "./schemas"; + +export function registerMediaTools(server: McpServer): void { + server.registerTool( + "list_media", + { + description: + "Returns a paginated list of media items. Optionally filter by access level (public or private) or group label.", + inputSchema: { + page: z + .number() + .int() + .min(1) + .optional() + .describe("Page number (default: 1)"), + limit: z + .number() + .int() + .min(1) + .optional() + .describe("Items per page (default: 10)"), + access: z + .enum(["public", "private"]) + .optional() + .describe("Filter by access level"), + group: z.string().optional().describe("Filter by group label"), + }, + outputSchema: mediaListSchema, + annotations: { + readOnlyHint: true, + idempotentHint: true, + openWorldHint: false, + destructiveHint: false, + }, + }, + async (args: any, extra: any) => { + const userId = extra.authInfo?.user?._id; + const apikey = extra.authInfo?.token; + if (!userId || !apikey) { + return AUTH_ERROR; + } + try { + const currentPage = args.page || 1; + const recordsPerPage = args.limit || 10; + const result = await mediaService.getPage({ + userId, + apikey, + access: args.access, + page: currentPage, + group: args.group, + recordsPerPage, + }); + return { + content: [ + { type: "text" as const, text: JSON.stringify(result) }, + ], + structuredContent: { mediaItems: result }, + }; + } catch { + return INTERNAL_ERROR; + } + }, + ); + + server.registerTool( + "get_media", + { + description: + "Returns full metadata for a single media item identified by its ID.", + inputSchema: { + mediaId: z.string().describe("Media item ID"), + }, + outputSchema: mediaSchema, + annotations: { + readOnlyHint: true, + idempotentHint: true, + openWorldHint: false, + destructiveHint: false, + }, + }, + async (args: any, extra: any) => { + const userId = extra.authInfo?.clientId; + const apikey = extra.authInfo?.token; + if (!userId || !apikey) { + return AUTH_ERROR; + } + try { + const media = await mediaService.getMediaDetails({ + userId, + apikey, + mediaId: args.mediaId, + }); + if (!media) { + return { + content: [ + { + type: "text" as const, + text: NOT_FOUND, + }, + ], + isError: true, + }; + } + return { + content: [ + { type: "text" as const, text: JSON.stringify(media) }, + ], + structuredContent: media as any, + }; + } catch { + return INTERNAL_ERROR; + } + }, + ); + + server.registerTool( + "get_media_count", + { + description: + "Returns the total number of media items in the default app.", + outputSchema: z.object({ count: z.number() }), + annotations: { + readOnlyHint: true, + idempotentHint: true, + openWorldHint: false, + destructiveHint: false, + }, + }, + async (extra: any) => { + const userId = extra.authInfo?.user?._id; + const apikey = extra.authInfo?.token; + if (!userId || !apikey) { + return AUTH_ERROR; + } + try { + const count = await mediaQueries.getMediaCount({ + userId, + apikey, + }); + return { + content: [ + { + type: "text" as const, + text: JSON.stringify({ count }), + }, + ], + structuredContent: { count }, + }; + } catch { + return INTERNAL_ERROR; + } + }, + ); + + server.registerTool( + "get_total_storage", + { + description: + "Returns the total storage used by the default app and the account storage limit, both in bytes.", + outputSchema: storageSchema, + annotations: { + readOnlyHint: true, + idempotentHint: true, + openWorldHint: false, + destructiveHint: false, + }, + }, + handleGetTotalStorageTool, + ); + + server.registerTool( + "delete_media", + { + description: + "Permanently deletes a media item. This action cannot be undone.", + inputSchema: { + mediaId: z.string().describe("ID of the media item to delete"), + }, + outputSchema: successMessageSchema, + annotations: { + readOnlyHint: false, + destructiveHint: true, + openWorldHint: true, + }, + }, + async (args: any, extra: any) => { + const userId = extra.authInfo?.clientId; + const apikey = extra.authInfo?.token; + if (!userId || !apikey) { + return AUTH_ERROR; + } + try { + await mediaService.deleteMedia({ + userId, + apikey, + mediaId: args.mediaId, + }); + const response = { message: SUCCESS }; + return { + content: [ + { + type: "text" as const, + text: JSON.stringify(response), + }, + ], + structuredContent: response, + }; + } catch { + return INTERNAL_ERROR; + } + }, + ); + + server.registerTool( + "seal_media", + { + description: + "Locks a media item to mark it for persistence. Once sealed, the item can no longer be modified.", + inputSchema: { + mediaId: z.string().describe("ID of the media item to seal"), + }, + outputSchema: mediaSchema, + annotations: { + readOnlyHint: false, + destructiveHint: false, + openWorldHint: true, + }, + }, + async (args: any, extra: any) => { + const userId = extra.authInfo?.clientId; + const apikey = extra.authInfo?.token; + if (!userId || !apikey) { + return AUTH_ERROR; + } + try { + const media = await mediaService.sealMedia({ + userId, + apikey, + mediaId: args.mediaId, + }); + return { + content: [ + { type: "text" as const, text: JSON.stringify(media) }, + ], + structuredContent: media as any, + }; + } catch { + return INTERNAL_ERROR; + } + }, + ); +} + +export async function handleGetTotalStorageTool( + extra: any, + dependencies = { getTotalSpace: mediaQueries.getTotalSpace }, +) { + const user = extra.authInfo?.user; + const userId = user?._id; + const apikey = extra.authInfo?.token; + if (!userId || !apikey) { + return AUTH_ERROR; + } + try { + const storage = await dependencies.getTotalSpace({ userId, apikey }); + const response = { + storage, + maxStorage: getSubscriptionStatus(user) + ? maxStorageAllowedSubscribed + : maxStorageAllowedNotSubscribed, + }; + return { + content: [ + { + type: "text" as const, + text: JSON.stringify(response), + }, + ], + structuredContent: response, + }; + } catch { + return INTERNAL_ERROR; + } +} diff --git a/apps/api/src/mcp/tools/responses.ts b/apps/api/src/mcp/tools/responses.ts new file mode 100644 index 00000000..0dd7771c --- /dev/null +++ b/apps/api/src/mcp/tools/responses.ts @@ -0,0 +1,19 @@ +export const AUTH_ERROR = { + content: [ + { + type: "text" as const, + text: "Authentication required: valid API credentials were not provided.", + }, + ], + isError: true, +}; + +export const INTERNAL_ERROR = { + content: [ + { + type: "text" as const, + text: "An error occurred while processing your request.", + }, + ], + isError: true, +}; diff --git a/apps/api/src/mcp/tools/schemas.ts b/apps/api/src/mcp/tools/schemas.ts new file mode 100644 index 00000000..560666ca --- /dev/null +++ b/apps/api/src/mcp/tools/schemas.ts @@ -0,0 +1,41 @@ +import { z } from "zod"; + +const accessSchema = z.enum(["public", "private"]); + +export const mediaSchema = z.object({ + mediaId: z.string(), + originalFileName: z.string(), + mimeType: z.string(), + size: z.number(), + access: accessSchema, + file: z.string(), + thumbnail: z.string().optional(), + caption: z.string().optional(), + group: z.string().optional(), +}); + +export const mediaListItemSchema = mediaSchema.omit({ file: true }); + +export const mediaListSchema = z.object({ + mediaItems: z.array(mediaListItemSchema), +}); + +export const storageSchema = z.object({ + storage: z.number(), + maxStorage: z.number(), +}); + +export const mediaSettingsSchema = z.object({ + useWebP: z.boolean(), + webpOutputQuality: z.number().min(0).max(100), + thumbnailWidth: z.number().positive(), + thumbnailHeight: z.number().positive(), +}); + +export const successMessageSchema = z.object({ + message: z.string(), +}); + +export const signatureSchema = z.object({ + signature: z.string(), +}); diff --git a/apps/api/src/mcp/tools/settings.ts b/apps/api/src/mcp/tools/settings.ts new file mode 100644 index 00000000..9a6a9cb3 --- /dev/null +++ b/apps/api/src/mcp/tools/settings.ts @@ -0,0 +1,115 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { getMediaSettings } from "../../media-settings/service"; +import { updateMediaSettings } from "../../media-settings/queries"; +import { SUCCESS } from "../../config/strings"; +import { AUTH_ERROR, INTERNAL_ERROR } from "./responses"; +import { mediaSettingsSchema, successMessageSchema } from "./schemas"; + +export function registerSettingsTools(server: McpServer): void { + // get_media_settings + server.registerTool( + "get_media_settings", + { + description: + "Returns the current image processing configuration for the account, including WebP conversion settings and thumbnail dimensions.", + outputSchema: mediaSettingsSchema, + annotations: { + readOnlyHint: true, + idempotentHint: true, + openWorldHint: false, + destructiveHint: false, + }, + }, + async (extra: any) => { + const userId = extra.authInfo?.clientId; + const apikey = extra.authInfo?.token; + if (!userId || !apikey) { + return AUTH_ERROR; + } + try { + const settings = await getMediaSettings(userId, apikey); + return { + content: [ + { + type: "text" as const, + text: JSON.stringify(settings), + }, + ], + structuredContent: (settings as any) ?? {}, + }; + } catch { + return INTERNAL_ERROR; + } + }, + ); + + // update_media_settings + server.registerTool( + "update_media_settings", + { + description: + "Overwrites image processing settings for the account. Supply only the fields you want to change; omitted fields retain their current values.", + inputSchema: { + useWebP: z + .boolean() + .optional() + .describe("Convert uploaded images to WebP format"), + webpOutputQuality: z + .number() + .int() + .min(0) + .max(100) + .optional() + .describe("WebP output quality 0–100"), + thumbnailWidth: z + .number() + .int() + .positive() + .optional() + .describe("Generated thumbnail width in pixels"), + thumbnailHeight: z + .number() + .int() + .positive() + .optional() + .describe("Generated thumbnail height in pixels"), + }, + outputSchema: successMessageSchema, + annotations: { + readOnlyHint: false, + destructiveHint: true, + openWorldHint: false, + }, + }, + async (args: any, extra: any) => { + const userId = extra.authInfo?.clientId; + const apikey = extra.authInfo?.token; + if (!userId || !apikey) { + return AUTH_ERROR; + } + try { + await updateMediaSettings({ + userId, + apikey, + useWebP: args.useWebP, + webpOutputQuality: args.webpOutputQuality, + thumbnailWidth: args.thumbnailWidth, + thumbnailHeight: args.thumbnailHeight, + }); + const response = { message: SUCCESS }; + return { + content: [ + { + type: "text" as const, + text: JSON.stringify(response), + }, + ], + structuredContent: response, + }; + } catch { + return INTERNAL_ERROR; + } + }, + ); +} diff --git a/apps/api/src/mcp/tools/signature.ts b/apps/api/src/mcp/tools/signature.ts new file mode 100644 index 00000000..0a1418a7 --- /dev/null +++ b/apps/api/src/mcp/tools/signature.ts @@ -0,0 +1,55 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { generateSignature } from "../../signature/service"; +import { AUTH_ERROR, INTERNAL_ERROR } from "./responses"; +import { signatureSchema } from "./schemas"; + +export function registerSignatureTool(server: McpServer): void { + server.registerTool( + "create_upload_signature", + { + description: + "Generates a time-limited HMAC signature that authorizes a direct client-side file upload to MediaLit storage. The returned signature must be passed by the client when initiating the upload.", + inputSchema: { + group: z + .string() + .optional() + .describe( + "Optional group label to associate with the uploaded file", + ), + }, + outputSchema: signatureSchema, + annotations: { + readOnlyHint: false, + idempotentHint: true, + openWorldHint: false, + destructiveHint: false, + }, + }, + async (args: any, extra: any) => { + const userId = extra.authInfo?.clientId; + const apikey = extra.authInfo?.token; + if (!userId || !apikey) { + return AUTH_ERROR; + } + try { + const signature = await generateSignature({ + userId, + apikey, + group: args.group, + }); + return { + content: [ + { + type: "text" as const, + text: JSON.stringify({ signature }), + }, + ], + structuredContent: { signature } as Record, + }; + } catch { + return INTERNAL_ERROR; + } + }, + ); +} diff --git a/apps/api/src/mcp/tools/upload.ts b/apps/api/src/mcp/tools/upload.ts new file mode 100644 index 00000000..182367a0 --- /dev/null +++ b/apps/api/src/mcp/tools/upload.ts @@ -0,0 +1,175 @@ +import os from "os"; +import path from "path"; +import { writeFile, mkdir, copyFile, mkdtemp, rm } from "fs/promises"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import mediaService from "../../media/service"; +import { validateUploadConstraints } from "../../media/storage-middleware"; +import { AUTH_ERROR } from "./responses"; +import { mediaSchema } from "./schemas"; + +function toolError(text: string) { + return { + content: [ + { + type: "text" as const, + text, + }, + ], + isError: true, + }; +} + +const BASE64_ERROR = { + content: [ + { + type: "text" as const, + text: "Invalid file data: could not decode base64 content.", + }, + ], + isError: true, +}; + +const UPLOAD_ERROR = { + content: [ + { + type: "text" as const, + text: "An error occurred while uploading the file.", + }, + ], + isError: true, +}; + +function isValidBase64(str: string): boolean { + // Base64 must be non-empty and match the expected pattern + if (str.length === 0) return false; + // Allow standard base64 with optional padding + return /^[A-Za-z0-9+/]*={0,2}$/.test(str); +} + +export function registerUploadTool(server: McpServer): void { + server.registerTool( + "upload_media", + { + description: + "Upload a file to MediaLit storage from base64-encoded content. The upload is temporary until sealed.", + inputSchema: { + fileBase64: z + .string() + .min(1, "Base64 content cannot be empty") + .describe("Base64-encoded file content"), + fileName: z + .string() + .min(1, "Filename cannot be empty") + .describe("Filename with extension"), + mimeType: z + .string() + .min(1, "MIME type cannot be empty") + .describe("MIME type of the file"), + caption: z + .string() + .optional() + .describe("Optional caption for the file"), + access: z + .enum(["public", "private"]) + .optional() + .describe("Access level (default: private)"), + group: z + .string() + .optional() + .describe("Group label for organizing files"), + }, + outputSchema: mediaSchema, + annotations: { + readOnlyHint: false, + destructiveHint: false, + openWorldHint: true, + }, + }, + handleUploadMediaTool, + ); +} + +export async function handleUploadMediaTool(args: any, extra: any) { + const userId = extra?.authInfo?.clientId; + const apikey = extra?.authInfo?.token; + const user = extra?.authInfo?.user; + if (!userId || !apikey || !user) { + return AUTH_ERROR; + } + + // Validate base64 + if (!isValidBase64(args.fileBase64)) { + return BASE64_ERROR; + } + + let buffer: Buffer; + try { + buffer = Buffer.from(args.fileBase64, "base64"); + // Check that the decode actually consumed content and + // that we didn't silently produce an empty buffer from garbage + if (buffer.length === 0) { + return BASE64_ERROR; + } + } catch { + return BASE64_ERROR; + } + + const validation = await validateUploadConstraints({ + size: buffer.length, + user, + }); + if (!validation.valid) { + return toolError(validation.error); + } + + const tempDir = await mkdtemp(path.join(os.tmpdir(), "mcp-upload-")); + const tempPath = path.join(tempDir, args.fileName); + + try { + await writeFile(tempPath, buffer); + + const uploadedFile = { + name: args.fileName, + mimetype: args.mimeType, + size: buffer.length, + tempFilePath: tempPath, + mv: (destPath: string, callback: (err?: any) => void) => { + mkdir(path.dirname(destPath), { recursive: true }) + .then(() => copyFile(tempPath, destPath)) + .then(() => callback(null)) + .catch((err) => callback(err)); + }, + }; + + const mediaId = await mediaService.upload({ + userId, + apikey, + file: uploadedFile, + access: args.access || "private", + caption: args.caption || "", + group: args.group, + }); + const media = await mediaService.getMediaDetails({ + userId, + apikey, + mediaId, + }); + + return { + content: [ + { + type: "text" as const, + text: JSON.stringify(media), + }, + ], + structuredContent: media as any, + }; + } catch (err) { + return UPLOAD_ERROR; + } finally { + rm(tempDir, { recursive: true, force: true }).catch(() => { + // Ignore cleanup errors + }); + } +} diff --git a/apps/api/src/media-settings/routes.ts b/apps/api/src/media-settings/routes.ts index 7cc98070..89f1cb97 100644 --- a/apps/api/src/media-settings/routes.ts +++ b/apps/api/src/media-settings/routes.ts @@ -4,6 +4,7 @@ import { updateMediaSettingsHandler, } from "./handlers"; import apikey from "../apikey/middleware"; +import { authenticatedApiLimiter } from "../auth/limiters"; export default (passport: any) => { const router = express.Router(); @@ -14,7 +15,7 @@ export default (passport: any) => { #swagger.tags = ['Settings'] #swagger.summary = 'Update Media Settings' #swagger.description = 'Update configuration for media processing.' - #swagger.security = [{ "apiKeyAuth": [] }] + #swagger.security = [{ "bearerAuth": [] }, { "apiKeyAuth": [] }] #swagger.requestBody = { required: true, content: { @@ -40,6 +41,7 @@ export default (passport: any) => { #swagger.responses[401] = { description: 'Unauthorized' } #swagger.responses[500] = { description: 'Internal Server Error' } */ + authenticatedApiLimiter, apikey, updateMediaSettingsHandler, ); @@ -50,7 +52,7 @@ export default (passport: any) => { #swagger.tags = ['Settings'] #swagger.summary = 'Get Media Settings' #swagger.description = 'Retrieve current media processing configuration.' - #swagger.security = [{ "apiKeyAuth": [] }] + #swagger.security = [{ "bearerAuth": [] }, { "apiKeyAuth": [] }] #swagger.responses[200] = { description: 'Settings retrieved successfully', content: { @@ -62,6 +64,7 @@ export default (passport: any) => { #swagger.responses[401] = { description: 'Unauthorized' } #swagger.responses[500] = { description: 'Internal Server Error' } */ + authenticatedApiLimiter, apikey, getMediaSettingsHandler, ); diff --git a/apps/api/src/media/handlers.ts b/apps/api/src/media/handlers.ts index 709b2289..dd9848e9 100644 --- a/apps/api/src/media/handlers.ts +++ b/apps/api/src/media/handlers.ts @@ -3,19 +3,13 @@ import { maxStorageAllowedNotSubscribed, maxStorageAllowedSubscribed, } from "../config/constants"; -import { - FILE_IS_REQUIRED, - FILE_SIZE_EXCEEDED, - NOT_FOUND, - SUCCESS, -} from "../config/strings"; +import { FILE_IS_REQUIRED, NOT_FOUND, SUCCESS } from "../config/strings"; import logger from "../services/log"; import { Request } from "express"; import mediaService from "./service"; import { getMediaCount as getCount, getTotalSpace } from "./queries"; import { getSubscriptionStatus } from "@medialit/models"; import { getSignatureFromReq } from "../signature/utils"; -import getMaxFileUploadSize from "./utils/get-max-file-upload-size"; import { getMediaSchema, uploadMediaSchema } from "./schemas"; function validateUploadOptions(req: Request): Joi.ValidationResult { @@ -34,13 +28,6 @@ export async function uploadMedia( return res.status(400).json({ error: FILE_IS_REQUIRED }); } - const allowedFileSize = getMaxFileUploadSize(req); - if (req.files.file.size > allowedFileSize) { - return res.status(400).json({ - error: `${FILE_SIZE_EXCEEDED}. Allowed: ${allowedFileSize} bytes`, - }); - } - const { error } = validateUploadOptions(req); if (error) { return res.status(400).json({ error: error.message }); @@ -182,13 +169,7 @@ export async function sealMedia(req: any, res: any) { mediaId, }); - const mediaDetails = await mediaService.getMediaDetails({ - userId: req.user.id, - apikey: req.apikey, - mediaId: media.mediaId, - }); - - return res.status(200).json(mediaDetails); + return res.status(200).json(media); } catch (err: any) { const statusCode = err.message === "Media not found" diff --git a/apps/api/src/media/queries.ts b/apps/api/src/media/queries.ts index e1ebe947..68f7d0d6 100644 --- a/apps/api/src/media/queries.ts +++ b/apps/api/src/media/queries.ts @@ -2,7 +2,39 @@ import mongoose, { FilterQuery } from "mongoose"; import { numberOfRecordsPerPage } from "../config/constants"; import GetPageProps from "./GetPageProps"; import MediaModel from "./model"; -import { Constants, type MediaWithUserId } from "@medialit/models"; +import { + AccessControl, + Constants, + type MediaWithUserId, +} from "@medialit/models"; + +export function buildMediaCountQuery({ + userId, + apikey, + access, + group, +}: { + userId: string | mongoose.Types.ObjectId; + apikey: string; + access?: AccessControl; + group?: string; +}): FilterQuery { + const query: FilterQuery = { + apikey, + userId, + temp: { $ne: true }, + }; + if (access) { + query.accessControl = + access === Constants.AccessControl.PRIVATE + ? Constants.AccessControl.PRIVATE + : Constants.AccessControl.PUBLIC; + } + if (typeof group === "string" && group.trim().length > 0) { + query.group = { $regex: `^${escapeRegex(group.trim())}` }; + } + return query; +} function escapeRegex(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); @@ -28,15 +60,16 @@ export async function getMedia({ export async function getMediaCount({ userId, apikey, + access, + group, }: { userId: string; apikey: string; + access?: AccessControl; + group?: string; }): Promise { - return await MediaModel.countDocuments({ - apikey, - userId, - temp: { $ne: true }, - }).lean(); + const query = buildMediaCountQuery({ userId, apikey, access, group }); + return await MediaModel.countDocuments(query).lean(); } export async function getTotalSpace({ @@ -82,20 +115,7 @@ export async function getPaginatedMedia({ group, recordsPerPage, }: GetPageProps): Promise { - const query: FilterQuery = { - userId, - apikey, - temp: { $ne: true }, - }; - if (access) { - query.accessControl = - access === Constants.AccessControl.PRIVATE - ? Constants.AccessControl.PRIVATE - : Constants.AccessControl.PUBLIC; - } - if (typeof group === "string" && group.trim().length > 0) { - query.group = { $regex: `^${escapeRegex(group.trim())}` }; - } + const query = buildMediaCountQuery({ userId, apikey, access, group }); const limitWithFallback = recordsPerPage || numberOfRecordsPerPage; return await MediaModel.find(query, { diff --git a/apps/api/src/media/routes.ts b/apps/api/src/media/routes.ts index cef933b4..b86253e7 100644 --- a/apps/api/src/media/routes.ts +++ b/apps/api/src/media/routes.ts @@ -18,6 +18,7 @@ import { import signatureMiddleware from "../signature/middleware"; import storage from "./storage-middleware"; import { getSignatureFromReq } from "../signature/utils"; +import { authenticatedApiLimiter } from "../auth/limiters"; const router = express.Router(); @@ -26,8 +27,8 @@ router.post( /* #swagger.tags = ['Media'] #swagger.summary = 'Upload Media' - #swagger.description = 'Upload a new media file. Use API key auth from Authorize (`x-medialit-apikey`) or pass `x-medialit-signature` for this endpoint only.' - #swagger.security = [{ "apiKeyAuth": [] }, { "signatureAuth": [] }] + #swagger.description = 'Upload a new media file. Use OAuth Bearer auth, API key auth from Authorize (`x-medialit-apikey`), or pass `x-medialit-signature` for this endpoint only.' + #swagger.security = [{ "bearerAuth": [] }, { "apiKeyAuth": [] }, { "signatureAuth": [] }] #swagger.parameters['x-medialit-signature'] = { in: 'header', description: 'Upload Signature for secure client-side uploads', @@ -73,6 +74,7 @@ router.post( #swagger.responses[500] = { description: 'Internal Server Error' } */ cors(), + authenticatedApiLimiter, fileUpload({ useTempFiles: true, tempFileDir: tempFileDirForUploads, @@ -105,7 +107,7 @@ router.post( #swagger.tags = ['Media'] #swagger.summary = 'Get Media Count' #swagger.description = 'Get the total number of media files.' - #swagger.security = [{ "apiKeyAuth": [] }] + #swagger.security = [{ "bearerAuth": [] }, { "apiKeyAuth": [] }] #swagger.responses[200] = { description: 'Count retrieved successfully', content: { @@ -116,6 +118,7 @@ router.post( } #swagger.responses[401] = { description: 'Unauthorized' } */ + authenticatedApiLimiter, apikey, getMediaCount, ); @@ -125,7 +128,7 @@ router.post( #swagger.tags = ['Media'] #swagger.summary = 'Get Total Size' #swagger.description = 'Get the total size of all media files in bytes.' - #swagger.security = [{ "apiKeyAuth": [] }] + #swagger.security = [{ "bearerAuth": [] }, { "apiKeyAuth": [] }] #swagger.responses[200] = { description: 'Size retrieved successfully', content: { @@ -136,6 +139,7 @@ router.post( } #swagger.responses[401] = { description: 'Unauthorized' } */ + authenticatedApiLimiter, apikey, getTotalSpaceOccupied, ); @@ -145,7 +149,7 @@ router.post( #swagger.tags = ['Media'] #swagger.summary = 'Get Media Details' #swagger.description = 'Retrieve metadata for a specific media item.' - #swagger.security = [{ "apiKeyAuth": [] }] + #swagger.security = [{ "bearerAuth": [] }, { "apiKeyAuth": [] }] #swagger.parameters['mediaId'] = { in: 'path', description: 'ID of the media file', @@ -164,6 +168,7 @@ router.post( #swagger.responses[404] = { description: 'Media not found' } #swagger.responses[500] = { description: 'Internal Server Error' } */ + authenticatedApiLimiter, apikey, getMediaDetails, ); @@ -173,7 +178,7 @@ router.post( #swagger.tags = ['Media'] #swagger.summary = 'List Media' #swagger.description = 'List media files filtered by optional query payload.' - #swagger.security = [{ "apiKeyAuth": [] }] + #swagger.security = [{ "bearerAuth": [] }, { "apiKeyAuth": [] }] #swagger.requestBody = { required: false, content: { @@ -197,6 +202,7 @@ router.post( #swagger.responses[401] = { description: 'Unauthorized' } #swagger.responses[500] = { description: 'Internal Server Error' } */ + authenticatedApiLimiter, apikey, getMedia, ); @@ -206,7 +212,7 @@ router.post( #swagger.tags = ['Media'] #swagger.summary = 'Seal Media' #swagger.description = 'Seal a media file (mark as processed/finalized).' - #swagger.security = [{ "apiKeyAuth": [] }] + #swagger.security = [{ "bearerAuth": [] }, { "apiKeyAuth": [] }] #swagger.parameters['mediaId'] = { in: 'path', description: 'ID of the media file', @@ -225,6 +231,7 @@ router.post( #swagger.responses[404] = { description: 'Media not found' } #swagger.responses[500] = { description: 'Internal Server Error' } */ + authenticatedApiLimiter, apikey, sealMedia, ); @@ -234,7 +241,7 @@ router.delete( #swagger.tags = ['Media'] #swagger.summary = 'Delete Media' #swagger.description = 'Permanently delete a media file.' - #swagger.security = [{ "apiKeyAuth": [] }] + #swagger.security = [{ "bearerAuth": [] }, { "apiKeyAuth": [] }] #swagger.parameters['mediaId'] = { in: 'path', description: 'ID of the media file', @@ -257,6 +264,7 @@ router.delete( #swagger.responses[401] = { description: 'Unauthorized' } #swagger.responses[500] = { description: 'Internal Server Error' } */ + authenticatedApiLimiter, apikey, deleteMedia, ); diff --git a/apps/api/src/media/service.ts b/apps/api/src/media/service.ts index e86cca13..280c8cf3 100644 --- a/apps/api/src/media/service.ts +++ b/apps/api/src/media/service.ts @@ -35,6 +35,7 @@ import GetPageProps from "./GetPageProps"; import { deleteMediaQuery, getMedia, + getMediaCount, getPaginatedMedia, createMedia, } from "./queries"; @@ -235,6 +236,20 @@ async function getPage({ return mappedResult; } +async function getMediaCountWithFilter({ + userId, + apikey, + access, + group, +}: GetPageProps): Promise { + return await getMediaCount({ + userId: String(userId), + apikey, + access, + group, + }); +} + async function getMediaDetails({ userId, apikey, @@ -253,6 +268,12 @@ async function getMediaDetails({ return null; } + return await getMediaResponse(media); +} + +async function getMediaResponse( + media: MediaWithUserId, +): Promise { // Determine file URL based on access control and temp status let fileUrl: string; if (media.temp || media.accessControl === Constants.AccessControl.PRIVATE) { @@ -366,14 +387,14 @@ async function sealMedia({ userId: string; apikey: string; mediaId: string; -}): Promise { +}): Promise { const media = await getMedia({ userId, apikey, mediaId }); if (!media) { throw new Error("Media not found"); } if (!media.temp) { - return media; + return await getMediaResponse(media); } const fileExtension = path.extname(media.fileName).replace(".", ""); @@ -470,12 +491,13 @@ async function sealMedia({ if (!updatedMedia) { throw new Error("Failed to retrieve updated media"); } - return updatedMedia; + return await getMediaResponse(updatedMedia); } export default { upload, getPage, + getMediaCount: getMediaCountWithFilter, getMediaDetails, deleteMedia, sealMedia, diff --git a/apps/api/src/media/storage-middleware.ts b/apps/api/src/media/storage-middleware.ts index 5486b011..e99e9b33 100644 --- a/apps/api/src/media/storage-middleware.ts +++ b/apps/api/src/media/storage-middleware.ts @@ -2,8 +2,18 @@ import { maxStorageAllowedNotSubscribed } from "../config/constants"; import { maxStorageAllowedSubscribed } from "../config/constants"; import { getSubscriptionStatus, User } from "@medialit/models"; import mediaQueries from "./queries"; -import { NOT_ENOUGH_STORAGE } from "../config/strings"; +import { FILE_SIZE_EXCEEDED, NOT_ENOUGH_STORAGE } from "../config/strings"; import mongoose from "mongoose"; +import getMaxFileUploadSize from "./utils/get-max-file-upload-size"; + +export type UploadValidationResult = + | { valid: true } + | { + valid: false; + reason: "file_size_exceeded" | "not_enough_storage"; + error: string; + allowedFileSize?: number; + }; export default async function storageValidation( req: any, @@ -16,15 +26,48 @@ export default async function storageValidation( }); } - if (!(await hasEnoughStorage((req.files.file as any).size, req.user))) { - return res.status(403).json({ - error: NOT_ENOUGH_STORAGE, + const validation = await validateUploadConstraints({ + size: (req.files.file as any).size, + user: req.user, + }); + if (!validation.valid) { + const status = validation.reason === "file_size_exceeded" ? 400 : 403; + return res.status(status).json({ + error: validation.error, }); } next(); } +export async function validateUploadConstraints({ + size, + user, +}: { + size: number; + user: User & { _id: mongoose.Types.ObjectId }; +}): Promise { + const allowedFileSize = getMaxFileUploadSize({ user }); + if (size > allowedFileSize) { + return { + valid: false, + reason: "file_size_exceeded", + error: `${FILE_SIZE_EXCEEDED}. Allowed: ${allowedFileSize} bytes`, + allowedFileSize, + }; + } + + if (!(await hasEnoughStorage(size, user))) { + return { + valid: false, + reason: "not_enough_storage", + error: NOT_ENOUGH_STORAGE, + }; + } + + return { valid: true }; +} + export async function hasEnoughStorage( size: number, user: User & { _id: mongoose.Types.ObjectId }, diff --git a/apps/api/src/oauth/__tests__/jwt.test.ts b/apps/api/src/oauth/__tests__/jwt.test.ts new file mode 100644 index 00000000..f9cba442 --- /dev/null +++ b/apps/api/src/oauth/__tests__/jwt.test.ts @@ -0,0 +1,107 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import crypto from "crypto"; +import jwt from "jsonwebtoken"; + +const TEST_KEY = crypto.randomBytes(32).toString("hex"); +process.env["OAUTH_SIGNING_KEY"] = TEST_KEY; + +let signAccessToken: any, + signRefreshToken: any, + verifyAccessToken: any, + verifyRefreshToken: any; +(async () => { + const mod = await import("../jwt.js"); + signAccessToken = mod.signAccessToken; + signRefreshToken = mod.signRefreshToken; + verifyAccessToken = mod.verifyAccessToken; + verifyRefreshToken = mod.verifyRefreshToken; +})(); + +async function waitForImport() { + while (!signAccessToken) await new Promise((r) => setTimeout(r, 10)); +} + +test("signAccessToken -> verifyAccessToken round trip", async () => { + await waitForImport(); + const token = signAccessToken("user-1", "client-1", ["read", "write"]); + const payload = verifyAccessToken(token); + assert.equal(payload?.sub, "user-1"); + assert.equal(payload?.cid, "client-1"); + assert.deepEqual(payload?.scope, ["read", "write"]); + assert.equal(typeof payload?.exp, "number"); +}); + +test("access token stores scope as an OAuth space-delimited string", async () => { + await waitForImport(); + const token = signAccessToken("user-1", "client-1", ["read", "write"]); + const decoded = jwt.decode(token) as any; + + assert.equal(decoded.scope, "read write"); +}); + +test("verifyAccessToken accepts legacy array scope tokens", async () => { + await waitForImport(); + const token = jwt.sign( + { + sub: "user-1", + cid: "client-1", + typ: "access", + scope: ["read", "write"], + }, + TEST_KEY, + { algorithm: "HS256" }, + ); + + const payload = verifyAccessToken(token); + assert.deepEqual(payload?.scope, ["read", "write"]); +}); + +test("signRefreshToken -> verifyRefreshToken round trip (jti present)", async () => { + await waitForImport(); + const token = signRefreshToken("user-1", "client-1"); + const payload = verifyRefreshToken(token); + assert.equal(payload?.sub, "user-1"); + assert.equal(payload?.cid, "client-1"); + assert.ok(payload?.jti, "refresh tokens must have a jti"); + assert.equal(typeof payload?.exp, "number"); +}); + +test("access token rejected as refresh token", async () => { + await waitForImport(); + const access = signAccessToken("user-1", "client-1"); + assert.equal(verifyRefreshToken(access), null); +}); + +test("refresh token rejected as access token", async () => { + await waitForImport(); + const refresh = signRefreshToken("user-1", "client-1"); + assert.equal(verifyAccessToken(refresh), null); +}); + +test("token signed with a different key is rejected", async () => { + await waitForImport(); + const other = crypto.randomBytes(32).toString("hex"); + const fake = jwt.sign({ cid: "evil", typ: "access" }, other, { + algorithm: "HS256", + subject: "evil-user", + expiresIn: 60, + }); + assert.equal(verifyAccessToken(fake), null); +}); + +test("garbage token is rejected", async () => { + await waitForImport(); + assert.equal(verifyAccessToken("not-a-jwt"), null); + assert.equal(verifyAccessToken(""), null); +}); + +test("expired access token is rejected", async () => { + await waitForImport(); + const expired = jwt.sign({ cid: "client-1", typ: "access" }, TEST_KEY, { + algorithm: "HS256", + subject: "user-1", + expiresIn: -10, + }); + assert.equal(verifyAccessToken(expired), null); +}); diff --git a/apps/api/src/oauth/__tests__/model.test.ts b/apps/api/src/oauth/__tests__/model.test.ts new file mode 100644 index 00000000..d4c3b45b --- /dev/null +++ b/apps/api/src/oauth/__tests__/model.test.ts @@ -0,0 +1,242 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import crypto from "crypto"; + +process.env["OAUTH_SIGNING_KEY"] = crypto.randomBytes(32).toString("hex"); + +test("registerClient persists DCR clients through OauthClient", async () => { + const clientModel = (await import("../client-model.js")).default as any; + const originalCreate = clientModel.create; + let saved: any; + clientModel.create = async (client: any) => { + saved = client; + return client; + }; + + try { + const { registerClient } = await import("../model.js"); + const response = await registerClient({ + redirect_uris: ["http://127.0.0.1:33418/"], + grant_types: ["authorization_code", "refresh_token"], + token_endpoint_auth_method: "none", + client_name: "VS Code", + scope: "read write", + }); + + assert.equal(saved.clientId, response.client_id); + assert.deepEqual(saved.redirectUris, response.redirect_uris); + assert.deepEqual(saved.grantTypes, [ + "authorization_code", + "refresh_token", + ]); + assert.equal(saved.tokenEndpointAuthMethod, "none"); + assert.equal(saved.clientName, "VS Code"); + assert.equal(saved.scope, "read write"); + assert.equal(response.client_secret_expires_at, 0); + } finally { + clientModel.create = originalCreate; + } +}); + +test("registerClient rejects non-loopback http redirect URIs", async () => { + const clientModel = (await import("../client-model.js")).default as any; + const originalCreate = clientModel.create; + let createCalled = false; + clientModel.create = async () => { + createCalled = true; + }; + + try { + const { DcrValidationError, registerClient } = await import( + "../model.js" + ); + + await assert.rejects( + registerClient({ + redirect_uris: ["http://example.com/callback"], + }), + DcrValidationError, + ); + assert.equal(createCalled, false); + } finally { + clientModel.create = originalCreate; + } +}); + +test("registerClient rejects redirect URIs with fragments or credentials", async () => { + const { DcrValidationError, registerClient } = await import("../model.js"); + + await assert.rejects( + registerClient({ + redirect_uris: ["https://example.com/callback#fragment"], + }), + DcrValidationError, + ); + + await assert.rejects( + registerClient({ + redirect_uris: ["https://user@example.com/callback"], + }), + DcrValidationError, + ); +}); + +test("registerClient rejects unsupported DCR grants", async () => { + const { DcrValidationError, registerClient } = await import("../model.js"); + + await assert.rejects( + registerClient({ + redirect_uris: ["http://127.0.0.1:33418/"], + grant_types: ["client_credentials"], + }), + DcrValidationError, + ); +}); + +test("redirectUriMatchesRegistered requires exact redirect URI match", async () => { + const { redirectUriMatchesRegistered } = await import("../helpers.js"); + const registered = ["https://example.com/callback"]; + + assert.equal( + redirectUriMatchesRegistered( + "https://example.com/callback", + registered, + ), + true, + ); + assert.equal( + redirectUriMatchesRegistered( + "https://example.com/callback?next=/evil", + registered, + ), + false, + ); +}); + +test("hashOtp binds OTP hashes to the pending session", async () => { + const { hashOtp } = await import("../helpers.js"); + + assert.equal( + hashOtp("pending-1", "123456"), + hashOtp("pending-1", "123456"), + ); + assert.notEqual( + hashOtp("pending-1", "123456"), + hashOtp("pending-2", "123456"), + ); +}); + +test("oauthModel.getClient resolves DCR clients from OauthClient", async () => { + const clientModel = (await import("../client-model.js")).default as any; + const originalFindOne = clientModel.findOne; + clientModel.findOne = () => ({ + lean: async () => ({ + clientId: "client-1", + redirectUris: ["http://127.0.0.1:33418/"], + grantTypes: ["authorization_code", "refresh_token"], + }), + }); + + try { + const { oauthModel } = await import("../model.js"); + const client = (await oauthModel.getClient("client-1", "")) as any; + + assert.equal(client?.id, "client-1"); + assert.deepEqual(client?.redirectUris, ["http://127.0.0.1:33418/"]); + assert.deepEqual(client?.grants, [ + "authorization_code", + "refresh_token", + ]); + } finally { + clientModel.findOne = originalFindOne; + } +}); + +test("oauthModel.saveToken uses expiry dates supplied by oauth2-server", async () => { + const { oauthModel } = await import("../model.js"); + const accessTokenExpiresAt = new Date("2030-01-01T00:00:00.000Z"); + const refreshTokenExpiresAt = new Date("2030-02-01T00:00:00.000Z"); + + const savedToken = (await oauthModel.saveToken( + { + accessTokenExpiresAt, + refreshTokenExpiresAt, + scope: ["read"], + } as any, + { id: "client-1" } as any, + { id: "user-1" } as any, + )) as any; + + assert.equal(savedToken.accessTokenExpiresAt, accessTokenExpiresAt); + assert.equal(savedToken.refreshTokenExpiresAt, refreshTokenExpiresAt); +}); + +test("oauthModel.getRefreshToken rejects persisted revoked refresh tokens", async () => { + const revokedTokenModel = (await import("../revoked-token-model.js")) + .default as any; + const originalFindOne = revokedTokenModel.findOne; + revokedTokenModel.findOne = () => ({ + lean: async () => ({ jti: "revoked-jti" }), + }); + + try { + const { oauthModel } = await import("../model.js"); + const savedToken = (await oauthModel.saveToken( + { scope: ["read"] } as any, + { id: "client-1" } as any, + { id: "user-1" } as any, + )) as any; + + const refreshToken = await oauthModel.getRefreshToken( + savedToken.refreshToken, + ); + + assert.equal(refreshToken, null); + } finally { + revokedTokenModel.findOne = originalFindOne; + } +}); + +test("oauthModel.revokeToken persists refresh token jti with expiry", async () => { + const revokedTokenModel = (await import("../revoked-token-model.js")) + .default as any; + const originalFindOne = revokedTokenModel.findOne; + const originalUpdateOne = revokedTokenModel.updateOne; + let update: any; + revokedTokenModel.findOne = () => ({ + lean: async () => null, + }); + revokedTokenModel.updateOne = async ( + filter: any, + payload: any, + options: any, + ) => { + update = { filter, payload, options }; + return { acknowledged: true, upsertedCount: 1 }; + }; + + try { + const { oauthModel } = await import("../model.js"); + const savedToken = (await oauthModel.saveToken( + { scope: ["read"] } as any, + { id: "client-1" } as any, + { id: "user-1" } as any, + )) as any; + const refreshToken = await oauthModel.getRefreshToken( + savedToken.refreshToken, + ); + + assert.ok(refreshToken); + await oauthModel.revokeToken(refreshToken); + + assert.equal(update.filter.jti, update.payload.$setOnInsert.jti); + assert.equal(update.payload.$setOnInsert.tokenType, "refresh_token"); + assert.equal(update.payload.$setOnInsert.userId, "user-1"); + assert.equal(update.payload.$setOnInsert.clientId, "client-1"); + assert.ok(update.payload.$setOnInsert.expiresAt instanceof Date); + assert.deepEqual(update.options, { upsert: true }); + } finally { + revokedTokenModel.findOne = originalFindOne; + revokedTokenModel.updateOne = originalUpdateOne; + } +}); diff --git a/apps/api/src/oauth/__tests__/rate-limit.test.ts b/apps/api/src/oauth/__tests__/rate-limit.test.ts new file mode 100644 index 00000000..dbc48a41 --- /dev/null +++ b/apps/api/src/oauth/__tests__/rate-limit.test.ts @@ -0,0 +1,15 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import rateLimit from "express-rate-limit"; + +test("rate limiters are valid express middleware functions", () => { + const limiter = rateLimit({ windowMs: 60_000, max: 10 }); + assert.equal(typeof limiter, "function"); + assert.equal(limiter.length, 3); +}); + +test("separate limiters have independent state", () => { + const a = rateLimit({ windowMs: 60_000, max: 10 }); + const b = rateLimit({ windowMs: 60_000, max: 30 }); + assert.notEqual(a, b); +}); diff --git a/apps/api/src/oauth/authorize-page.ts b/apps/api/src/oauth/authorize-page.ts new file mode 100644 index 00000000..de499ce0 --- /dev/null +++ b/apps/api/src/oauth/authorize-page.ts @@ -0,0 +1,339 @@ +import { Response as ExpressRes } from "express"; + +export function authorizePage(pendingId: string, clientId: string): string { + return ` + + + + +Authorize MediaLit + + + +
+ +
+
+
M
+ MediaLit +
+ +
+
+ "Instead of rebuilding file uploads and processing pipelines for every project, I just plug in MediaLit. Now my apps, my agents, and I can all work seamlessly on the same set of files." +
+ +
+
R
+
+
Rajat
+
Software Engineer
+
+
+
+ + +
+ + +
+
+
+

Get started

+

Enter your email to sign in or create an account

+ +
+ + +
+
+ + +
+ +
+

Verify OTP

+

Enter the code sent to

+ +
+ + +
+
+ + +
+ + +
+
+
+ + + +`; +} + +export function errorPage(res: ExpressRes, msg: string) { + res.type("html").status(400).send(` +Error + +

Authorization Error

${msg}

`); +} diff --git a/apps/api/src/oauth/client-model.ts b/apps/api/src/oauth/client-model.ts new file mode 100644 index 00000000..eaf29306 --- /dev/null +++ b/apps/api/src/oauth/client-model.ts @@ -0,0 +1,5 @@ +import mongoose from "mongoose"; +import { OauthClientSchema } from "@medialit/models"; + +export default mongoose.models.OauthClient || + mongoose.model("OauthClient", OauthClientSchema); diff --git a/apps/api/src/oauth/helpers.ts b/apps/api/src/oauth/helpers.ts new file mode 100644 index 00000000..9a17f964 --- /dev/null +++ b/apps/api/src/oauth/helpers.ts @@ -0,0 +1,43 @@ +import crypto from "crypto"; +import { Request as ExpressReq } from "express"; + +export const OTP_TTL_MS = 5 * 60 * 1000; +export const PENDING_AUTH_TTL_MS = 10 * 60 * 1000; +export const OTP_RESEND_COOLDOWN_MS = 60 * 1000; +export const MAX_OTP_ATTEMPTS = 5; + +export function generateOtp(): string { + return String(Math.floor(100000 + crypto.randomInt(0, 900000))); +} + +export function hashOtp(pendingId: string, otp: string): string { + return crypto + .createHmac("sha256", process.env.OAUTH_SIGNING_KEY || "") + .update(`${pendingId}:${otp}`) + .digest("hex"); +} + +export function generatePendingId(): string { + return crypto.randomBytes(16).toString("hex"); +} + +export function singleQueryParam(val: unknown): string | undefined { + if (Array.isArray(val)) return val.length > 0 ? String(val[0]) : undefined; + if (val === undefined || val === null) return undefined; + return String(val); +} + +export function redirectUriMatchesRegistered( + redirectUri: string, + registeredUris: string[], +): boolean { + return registeredUris.some((uri) => redirectUri === uri); +} + +export function reqHeadersHost(req: ExpressReq): string | null { + const forwarded = req.headers["x-forwarded-host"]; + if (forwarded) return Array.isArray(forwarded) ? forwarded[0] : forwarded; + const host = req.headers.host; + if (host) return Array.isArray(host) ? host[0] : host; + return null; +} diff --git a/apps/api/src/oauth/jwt.ts b/apps/api/src/oauth/jwt.ts new file mode 100644 index 00000000..1bb393b7 --- /dev/null +++ b/apps/api/src/oauth/jwt.ts @@ -0,0 +1,89 @@ +import jwt from "jsonwebtoken"; +import { randomUUID } from "crypto"; + +const KEYS = (process.env.OAUTH_SIGNING_KEY || "") + .split(",") + .map((s) => s.trim()) + .filter(Boolean); + +const SIGNING_KEY = KEYS[0]; +const VERIFY_KEYS = KEYS; + +const ACCESS_TOKEN_TTL = Number(process.env.TOKEN_TTL_SECONDS) || 900; +const REFRESH_TOKEN_TTL = 60 * 60 * 24 * 30; // 30 days + +export function signAccessToken( + userId: string, + clientId: string, + scope: string[] = [], +): string { + return jwt.sign( + { sub: userId, cid: clientId, typ: "access", scope: scope.join(" ") }, + SIGNING_KEY, + { algorithm: "HS256", expiresIn: ACCESS_TOKEN_TTL, noTimestamp: false }, + ); +} + +export function signRefreshToken(userId: string, clientId: string): string { + return jwt.sign( + { + sub: userId, + cid: clientId, + typ: "refresh", + jti: randomUUID(), + }, + SIGNING_KEY, + { algorithm: "HS256", expiresIn: REFRESH_TOKEN_TTL }, + ); +} + +export function verifyToken( + token: string, + expectedType: "access" | "refresh", +): { + sub: string; + cid: string; + scope: string[]; + exp?: number; + jti?: string; +} | null { + for (const key of VERIFY_KEYS) { + try { + const decoded = jwt.verify(token, key, { + algorithms: ["HS256"], + }) as any; + if (decoded.typ !== expectedType) return null; + return { + sub: decoded.sub, + cid: decoded.cid, + scope: normalizeScope(decoded.scope), + exp: decoded.exp, + jti: decoded.jti, + }; + } catch { + continue; + } + } + return null; +} + +export function verifyAccessToken(token: string) { + return verifyToken(token, "access"); +} + +export function verifyRefreshToken(token: string) { + return verifyToken(token, "refresh"); +} + +function normalizeScope(scope: unknown): string[] { + if (Array.isArray(scope)) { + return scope.filter((item): item is string => typeof item === "string"); + } + if (typeof scope === "string") { + return scope.split(/\s+/).filter(Boolean); + } + return []; +} + +export const ACCESS_TOKEN_TTL_SECONDS = ACCESS_TOKEN_TTL; +export const REFRESH_TOKEN_TTL_SECONDS = REFRESH_TOKEN_TTL; diff --git a/apps/api/src/oauth/limiters.ts b/apps/api/src/oauth/limiters.ts new file mode 100644 index 00000000..c8978956 --- /dev/null +++ b/apps/api/src/oauth/limiters.ts @@ -0,0 +1,67 @@ +import rateLimit from "express-rate-limit"; + +export const authorizeLimiter = rateLimit({ + windowMs: 60_000, + max: 10, + standardHeaders: true, + legacyHeaders: false, + message: { + error: "too_many_requests", + error_description: "Too many requests, please try again later.", + }, +}); + +export const verifyOtpLimiter = rateLimit({ + windowMs: 60_000, + max: 10, + standardHeaders: true, + legacyHeaders: false, + message: { + error: "too_many_requests", + error_description: "Too many requests, please try again later.", + }, +}); + +export const tokenLimiter = rateLimit({ + windowMs: 60_000, + max: 30, + standardHeaders: true, + legacyHeaders: false, + message: { + error: "too_many_requests", + error_description: "Too many requests, please try again later.", + }, +}); + +export const revokeLimiter = rateLimit({ + windowMs: 60_000, + max: 30, + standardHeaders: true, + legacyHeaders: false, + message: { + error: "too_many_requests", + error_description: "Too many requests.", + }, +}); + +export const userinfoLimiter = rateLimit({ + windowMs: 60_000, + max: 30, + standardHeaders: true, + legacyHeaders: false, + message: { + error: "too_many_requests", + error_description: "Too many requests.", + }, +}); + +export const registerLimiter = rateLimit({ + windowMs: 60_000, + max: 10, + standardHeaders: true, + legacyHeaders: false, + message: { + error: "too_many_requests", + error_description: "Too many client registration requests.", + }, +}); diff --git a/apps/api/src/oauth/middleware.ts b/apps/api/src/oauth/middleware.ts new file mode 100644 index 00000000..e2b90453 --- /dev/null +++ b/apps/api/src/oauth/middleware.ts @@ -0,0 +1,13 @@ +import { verifyAccessToken } from "./jwt"; + +export async function validateBearerToken( + bearer: string, +): Promise<{ userId: string; clientId: string; scopes: string[] } | null> { + const payload = verifyAccessToken(bearer); + if (!payload) return null; + return { + userId: payload.sub, + clientId: payload.cid, + scopes: payload.scope, + }; +} diff --git a/apps/api/src/oauth/model.ts b/apps/api/src/oauth/model.ts new file mode 100644 index 00000000..3c71d544 --- /dev/null +++ b/apps/api/src/oauth/model.ts @@ -0,0 +1,387 @@ +import crypto from "crypto"; +import type OAuth2Server from "@node-oauth/oauth2-server"; +import logger from "../services/log"; +import OauthClient from "./client-model"; +import OauthRevokedToken from "./revoked-token-model"; +import { + signAccessToken, + signRefreshToken, + verifyAccessToken, + verifyRefreshToken, + ACCESS_TOKEN_TTL_SECONDS, + REFRESH_TOKEN_TTL_SECONDS, +} from "./jwt"; + +interface StoredAuthorizationCode { + authorizationCode: string; + expiresAt: Date; + redirectUri: string; + scope?: string[]; + client: OAuth2Server.Client; + user: OAuth2Server.User; + codeChallenge?: string; + codeChallengeMethod?: string; +} + +interface StoredAccessToken { + accessToken: string; + accessTokenExpiresAt?: Date; + scope?: string[]; + client: OAuth2Server.Client; + user: OAuth2Server.User; +} + +interface StoredRefreshToken { + refreshToken: string; + refreshTokenExpiresAt?: Date; + scope?: string[]; + client: OAuth2Server.Client; + user: OAuth2Server.User; +} + +const authorizationCodes = new Map(); + +const authorizationCodeSweep = setInterval( + () => { + const now = new Date(); + authorizationCodes.forEach((data, code) => { + if (data.expiresAt < now) authorizationCodes.delete(code); + }); + }, + 5 * 60 * 1000, +); +authorizationCodeSweep.unref?.(); + +const STATIC_CLIENTS: Record = { + "web-client": { + redirectUris: [ + `${process.env.WEB_CLIENT || "http://localhost:3000"}/api/auth/callback/medialit`, + ].filter((uri): uri is string => !!uri), + }, + "mobile-app": { + redirectUris: ["medialit://oauth/callback"], + }, +}; + +interface DynamicClient { + clientId: string; + clientIdIssuedAt: number; + redirectUris: string[]; + grantTypes: string[]; + tokenEndpointAuthMethod: string; + clientName?: string; + scope?: string; +} + +export class DcrValidationError extends Error { + constructor(message: string) { + super(message); + this.name = "DcrValidationError"; + } +} + +const MAX_REDIRECT_URIS = 10; +const MAX_REDIRECT_URI_LENGTH = 2048; +const ALLOWED_DCR_GRANTS = ["authorization_code", "refresh_token"]; + +function isLoopbackRedirect(url: URL): boolean { + return ( + url.hostname === "localhost" || + url.hostname === "127.0.0.1" || + url.hostname === "[::1]" || + url.hostname === "::1" + ); +} + +function validateRedirectUri(uri: unknown): string { + if (typeof uri !== "string") { + throw new DcrValidationError("redirect_uri must be a string"); + } + + if (uri.length > MAX_REDIRECT_URI_LENGTH) { + throw new DcrValidationError("redirect_uri is too long"); + } + + let parsed: URL; + try { + parsed = new URL(uri); + } catch { + throw new DcrValidationError("redirect_uri must be a valid URL"); + } + + if (parsed.hash) { + throw new DcrValidationError( + "redirect_uri must not contain a fragment", + ); + } + + if (parsed.username || parsed.password) { + throw new DcrValidationError( + "redirect_uri must not contain credentials", + ); + } + + if (parsed.protocol !== "https:") { + if (parsed.protocol !== "http:" || !isLoopbackRedirect(parsed)) { + throw new DcrValidationError( + "redirect_uri must use HTTPS unless it is localhost or loopback", + ); + } + } + + return parsed.toString(); +} + +function validateGrantTypes(grantTypes?: unknown): string[] { + const grants = grantTypes ?? ["authorization_code"]; + if (!Array.isArray(grants) || grants.length === 0) { + throw new DcrValidationError("grant_types must be a non-empty array"); + } + const invalid = grants.find((grant) => !ALLOWED_DCR_GRANTS.includes(grant)); + if (invalid) { + throw new DcrValidationError(`Unsupported grant_type: ${invalid}`); + } + return grants; +} + +function validateTokenEndpointAuthMethod(method: string): "none" { + if (method !== "none") { + throw new DcrValidationError("token_endpoint_auth_method must be none"); + } + return "none"; +} + +function sanitizeDcrClient(client: DynamicClient): DynamicClient { + if (!Array.isArray(client.redirectUris)) { + throw new DcrValidationError("redirect_uris must be an array"); + } + + if (client.redirectUris.length === 0) { + throw new DcrValidationError("At least one redirect_uri is required"); + } + + if (client.redirectUris.length > MAX_REDIRECT_URIS) { + throw new DcrValidationError("Too many redirect_uris"); + } + + return { + clientId: client.clientId, + clientIdIssuedAt: client.clientIdIssuedAt, + redirectUris: client.redirectUris.map(validateRedirectUri), + grantTypes: validateGrantTypes(client.grantTypes), + tokenEndpointAuthMethod: validateTokenEndpointAuthMethod( + client.tokenEndpointAuthMethod, + ), + clientName: client.clientName, + scope: client.scope, + }; +} + +export interface DcrRequest { + redirect_uris: string[]; + grant_types?: string[]; + token_endpoint_auth_method?: string; + client_name?: string; + scope?: string; +} + +export interface DcrResponse { + client_id: string; + client_id_issued_at: number; + client_secret_expires_at: number; + redirect_uris: string[]; + grant_types: string[]; + token_endpoint_auth_method: string; + client_name?: string; + scope?: string; +} + +export async function registerClient(meta: DcrRequest): Promise { + const clientId = crypto.randomUUID(); + const now = Math.floor(Date.now() / 1000); + const client: DynamicClient = { + clientId, + clientIdIssuedAt: now, + redirectUris: meta.redirect_uris, + grantTypes: meta.grant_types ?? ["authorization_code"], + tokenEndpointAuthMethod: meta.token_endpoint_auth_method ?? "none", + clientName: meta.client_name, + scope: meta.scope, + }; + const sanitized = sanitizeDcrClient(client); + await OauthClient.create(sanitized); + return { + client_id: sanitized.clientId, + client_id_issued_at: sanitized.clientIdIssuedAt, + client_secret_expires_at: 0, + redirect_uris: sanitized.redirectUris, + grant_types: sanitized.grantTypes, + token_endpoint_auth_method: sanitized.tokenEndpointAuthMethod, + client_name: sanitized.clientName, + scope: sanitized.scope, + }; +} + +export const oauthModel: OAuth2Server.AuthorizationCodeModel & + OAuth2Server.RefreshTokenModel = { + async getClient(clientId: string, _clientSecret?: string) { + const dyn = (await OauthClient.findOne({ + clientId, + }).lean()) as DynamicClient | null; + if (dyn) { + return { + id: dyn.clientId, + redirectUris: dyn.redirectUris, + grants: dyn.grantTypes, + accessTokenLifetime: ACCESS_TOKEN_TTL_SECONDS, + refreshTokenLifetime: 60 * 60 * 24 * 30, + }; + } + const stat = STATIC_CLIENTS[clientId]; + if (stat) { + return { + id: clientId, + redirectUris: stat.redirectUris, + grants: ["authorization_code", "refresh_token"], + accessTokenLifetime: ACCESS_TOKEN_TTL_SECONDS, + refreshTokenLifetime: 60 * 60 * 24 * 30, // 30 days + }; + } + logger.warn({ clientId }, "Unknown client_id rejected"); + return null; + }, + + async saveAuthorizationCode( + code: Pick< + StoredAuthorizationCode, + | "authorizationCode" + | "expiresAt" + | "redirectUri" + | "scope" + | "codeChallenge" + | "codeChallengeMethod" + >, + client: OAuth2Server.Client, + user: OAuth2Server.User, + ): Promise { + const stored: StoredAuthorizationCode = { + authorizationCode: code.authorizationCode, + expiresAt: code.expiresAt, + redirectUri: code.redirectUri, + scope: code.scope, + client, + user, + codeChallenge: code.codeChallenge, + codeChallengeMethod: code.codeChallengeMethod, + }; + authorizationCodes.set(code.authorizationCode, stored); + return stored; + }, + + async getAuthorizationCode( + authorizationCode: string, + ): Promise { + const stored = authorizationCodes.get(authorizationCode); + if (!stored) return null; + return stored; + }, + + async revokeAuthorizationCode( + code: StoredAuthorizationCode, + ): Promise { + authorizationCodes.delete(code.authorizationCode); + return true; + }, + + async saveToken( + token: Partial, + client: OAuth2Server.Client, + user: OAuth2Server.User, + ): Promise { + const userId = String((user as any).id); + const clientId = String((client as any).id); + const scope = token.scope; + + const accessToken = signAccessToken(userId, clientId, scope); + const refreshToken = signRefreshToken(userId, clientId); + + return { + accessToken, + accessTokenExpiresAt: token.accessTokenExpiresAt, + refreshToken, + refreshTokenExpiresAt: token.refreshTokenExpiresAt, + scope, + client, + user, + custom: { userId }, + }; + }, + + async getAccessToken( + accessToken: string, + ): Promise { + const payload = verifyAccessToken(accessToken); + if (!payload) return null; + return { + accessToken, + accessTokenExpiresAt: payload.exp + ? new Date(payload.exp * 1000) + : new Date(Date.now() + ACCESS_TOKEN_TTL_SECONDS * 1000), + scope: payload.scope, + client: { id: payload.cid } as OAuth2Server.Client, + user: { id: payload.sub } as OAuth2Server.User, + }; + }, + + async getRefreshToken( + refreshToken: string, + ): Promise { + const payload = verifyRefreshToken(refreshToken); + if (!payload) return null; + if (payload.jti) { + const revoked = await OauthRevokedToken.findOne({ + jti: payload.jti, + }).lean(); + if (revoked) return null; + } + return { + refreshToken, + refreshTokenExpiresAt: payload.exp + ? new Date(payload.exp * 1000) + : new Date(Date.now() + REFRESH_TOKEN_TTL_SECONDS * 1000), + scope: payload.scope, + client: { id: payload.cid } as OAuth2Server.Client, + user: { id: payload.sub } as OAuth2Server.User, + }; + }, + + async revokeToken(token: StoredRefreshToken): Promise { + const payload = verifyRefreshToken(token.refreshToken); + if (payload?.jti) { + await OauthRevokedToken.updateOne( + { jti: payload.jti }, + { + $setOnInsert: { + jti: payload.jti, + tokenType: "refresh_token", + userId: payload.sub, + clientId: payload.cid, + expiresAt: payload.exp + ? new Date(payload.exp * 1000) + : (token.refreshTokenExpiresAt ?? + new Date( + Date.now() + REFRESH_TOKEN_TTL_SECONDS * 1000, + )), + revokedAt: new Date(), + }, + }, + { upsert: true }, + ); + } + return true; + }, + + async verifyScope(_token: OAuth2Server.Token, _scope: string[]) { + return true; + }, +}; diff --git a/apps/api/src/oauth/pending-auth-model.ts b/apps/api/src/oauth/pending-auth-model.ts new file mode 100644 index 00000000..4d0885e1 --- /dev/null +++ b/apps/api/src/oauth/pending-auth-model.ts @@ -0,0 +1,5 @@ +import mongoose from "mongoose"; +import { OauthPendingAuthSchema } from "@medialit/models"; + +export default mongoose.models.OauthPendingAuth || + mongoose.model("OauthPendingAuth", OauthPendingAuthSchema); diff --git a/apps/api/src/oauth/revoked-token-model.ts b/apps/api/src/oauth/revoked-token-model.ts new file mode 100644 index 00000000..bea528f7 --- /dev/null +++ b/apps/api/src/oauth/revoked-token-model.ts @@ -0,0 +1,5 @@ +import mongoose from "mongoose"; +import { OauthRevokedTokenSchema } from "@medialit/models"; + +export default mongoose.models.OauthRevokedToken || + mongoose.model("OauthRevokedToken", OauthRevokedTokenSchema); diff --git a/apps/api/src/oauth/routes.ts b/apps/api/src/oauth/routes.ts new file mode 100644 index 00000000..09ca95a3 --- /dev/null +++ b/apps/api/src/oauth/routes.ts @@ -0,0 +1,508 @@ +import { Router, Request as ExpressReq, Response as ExpressRes } from "express"; +import { z } from "zod"; +import type { OauthPendingAuth as PendingAuthRecord } from "@medialit/models"; +import { DcrValidationError, oauthModel, registerClient } from "./model"; +import type { DcrRequest, DcrResponse } from "./model"; +import { OAuth2Server, oauth } from "./server"; +import { authorizePage, errorPage } from "./authorize-page"; +import { findByEmail, createUser, getUser } from "../user/queries"; +import { verifyAccessToken } from "./jwt"; +import logger from "../services/log"; +import OauthPendingAuth from "./pending-auth-model"; +import { + authorizeLimiter, + registerLimiter, + revokeLimiter, + tokenLimiter, + userinfoLimiter, + verifyOtpLimiter, +} from "./limiters"; +import { + generateOtp, + generatePendingId, + hashOtp, + MAX_OTP_ATTEMPTS, + OTP_TTL_MS, + OTP_RESEND_COOLDOWN_MS, + PENDING_AUTH_TTL_MS, + redirectUriMatchesRegistered, + reqHeadersHost, + singleQueryParam, +} from "./helpers"; + +export const oauthRouter = Router(); + +oauthRouter.get( + "/.well-known/oauth-authorization-server", + (_req: ExpressReq, res: ExpressRes) => { + const host = reqHeadersHost(_req) || "localhost:8000"; + const baseUrl = host.includes("localhost") + ? `http://${host}` + : `https://${host}`; + res.json({ + issuer: baseUrl, + authorization_endpoint: `${baseUrl}/oauth/authorize`, + token_endpoint: `${baseUrl}/oauth/token`, + revocation_endpoint: `${baseUrl}/oauth/revoke`, + registration_endpoint: `${baseUrl}/oauth/register`, + response_types_supported: ["code"], + grant_types_supported: ["authorization_code", "refresh_token"], + code_challenge_methods_supported: ["S256"], + token_endpoint_auth_methods_supported: ["none"], + }); + }, +); + +oauthRouter.get( + "/oauth/authorize", + authorizeLimiter, + async (req: ExpressReq, res: ExpressRes) => { + try { + const parsed = z + .object({ + response_type: z.literal("code"), + client_id: z.string().min(1, "Missing client_id."), + redirect_uri: z.string().min(1, "Missing redirect_uri."), + code_challenge: z + .string() + .min(1, "Missing code_challenge (PKCE required)."), + code_challenge_method: z.string().optional(), + state: z.string().optional(), + scope: z.string().optional(), + }) + .safeParse({ + response_type: singleQueryParam(req.query.response_type), + client_id: singleQueryParam(req.query.client_id), + redirect_uri: singleQueryParam(req.query.redirect_uri), + code_challenge: singleQueryParam(req.query.code_challenge), + code_challenge_method: singleQueryParam( + req.query.code_challenge_method, + ), + state: singleQueryParam(req.query.state), + scope: singleQueryParam(req.query.scope), + }); + + if (!parsed.success) { + return errorPage(res, parsed.error.errors[0].message); + } + + const q = parsed.data; + + const client = await oauthModel.getClient(q.client_id, ""); + if (!client) { + return errorPage(res, "Invalid client_id."); + } + + const uris = client.redirectUris as string[] | undefined; + if (uris && uris.length > 0) { + const matched = redirectUriMatchesRegistered( + q.redirect_uri, + uris, + ); + if (!matched) { + return errorPage( + res, + "redirect_uri does not match registered client.", + ); + } + } + + const pendingId = generatePendingId(); + await OauthPendingAuth.create({ + clientId: q.client_id, + redirectUri: q.redirect_uri, + codeChallenge: q.code_challenge, + codeChallengeMethod: q.code_challenge_method, + state: q.state, + scope: q.scope, + pendingId, + expiresAt: new Date(Date.now() + PENDING_AUTH_TTL_MS), + }); + + res.type("html").send(authorizePage(pendingId, q.client_id)); + } catch (err: any) { + logger.error({ error: err.message }, "OAuth authorize page error"); + errorPage(res, "An error occurred. Please try again."); + } + }, +); + +oauthRouter.post( + "/oauth/authorize/send-otp", + verifyOtpLimiter, + async (req: ExpressReq, res: ExpressRes) => { + try { + const { pendingId, email } = req.body || {}; + if (!pendingId || !email) { + return res.json({ + success: false, + error: "Missing pendingId or email", + }); + } + if (!/^[0-9a-f]{32}$/.test(String(pendingId))) { + return res.json({ success: false, error: "Invalid request" }); + } + if ( + !/^[^\s@]+@[^\s@.]+\.[^\s@]+$/.test(String(email).slice(0, 320)) + ) { + return res.json({ + success: false, + error: "Invalid email address", + }); + } + + const pending = (await OauthPendingAuth.findOne({ + pendingId: String(pendingId), + expiresAt: { $gt: new Date() }, + }).lean()) as PendingAuthRecord | null; + if (!pending) { + return res.json({ + success: false, + error: "Authorization session expired. Please go back and try again.", + }); + } + + if ( + pending.otpSentAt && + Date.now() - pending.otpSentAt.getTime() < + OTP_RESEND_COOLDOWN_MS + ) { + return res.json({ + success: false, + error: "Please wait before requesting another code.", + }); + } + + const otp = generateOtp(); + const emailValue = String(email); + await OauthPendingAuth.updateOne( + { pendingId: String(pendingId) }, + { + $set: { + email: emailValue, + otpHash: hashOtp(String(pendingId), otp), + otpExpires: new Date(Date.now() + OTP_TTL_MS), + otpSentAt: new Date(), + otpAttempts: 0, + }, + }, + ); + + if (process.env.NODE_ENV !== "production") { + logger.info({ email: emailValue, otp }, "[Dev] OTP generated"); + return res.json({ success: true, otp }); + } else { + try { + const nodemailer = require("nodemailer"); + if (process.env.EMAIL_HOST && process.env.EMAIL_USER) { + const transporter = nodemailer.createTransport({ + host: process.env.EMAIL_HOST, + port: Number(process.env.EMAIL_PORT) || 587, + auth: { + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_PASS, + }, + }); + await transporter.sendMail({ + from: process.env.EMAIL_FROM, + to: email, + subject: "Your MediaLit verification code", + text: `Enter this code to authorize: ${otp}`, + html: `

Enter this code to authorize MediaLit access:

${otp}

`, + }); + } + } catch (mailErr: any) { + logger.error( + { email, error: mailErr.message }, + "Failed to send OTP email", + ); + } + } + + res.json({ success: true }); + } catch (err: any) { + logger.error({ error: err.message }, "Send OTP error"); + res.json({ + success: false, + error: "Failed to send verification code", + }); + } + }, +); + +oauthRouter.post( + "/oauth/authorize/verify-otp", + verifyOtpLimiter, + async (req: ExpressReq, res: ExpressRes) => { + try { + const parsed = z + .object({ + pendingId: z + .string() + .regex(/^[0-9a-f]{32}$/, "Invalid request"), + otp: z.string().regex(/^\d{6}$/, "Invalid code format"), + }) + .safeParse(req.body || {}); + + if (!parsed.success) { + return res.json({ + success: false, + error: parsed.error.errors[0].message, + }); + } + + const { pendingId, otp } = parsed.data; + + const pending = (await OauthPendingAuth.findOneAndUpdate( + { + pendingId, + expiresAt: { $gt: new Date() }, + }, + { $inc: { otpAttempts: 1 } }, + { new: true }, + ).lean()) as PendingAuthRecord | null; + if (!pending) { + return res.json({ + success: false, + error: "Session expired. Please restart authorization.", + }); + } + + if ((pending.otpAttempts || 0) > MAX_OTP_ATTEMPTS) { + await OauthPendingAuth.deleteOne({ pendingId }); + return res.json({ + success: false, + error: "Too many attempts. Please restart authorization.", + }); + } + + if ( + !pending.otpHash || + !pending.otpExpires || + pending.otpExpires < new Date() + ) { + await OauthPendingAuth.deleteOne({ pendingId }); + return res.json({ + success: false, + error: "Code expired. Please restart authorization.", + }); + } + + if (pending.otpHash !== hashOtp(pendingId, String(otp))) { + return res.json({ success: false, error: "Invalid code." }); + } + + const email = pending.email!; + let user = await findByEmail(email); + if (!user) { + user = await createUser(email, undefined, "subscribed"); + } + const userId = String((user as any)._id || (user as any).id); + + const oauthReq = new OAuth2Server.Request({ + headers: { + "content-type": "application/x-www-form-urlencoded", + ...Object.fromEntries( + Object.entries(req.headers).map(([k, v]) => [ + k, + String(v), + ]), + ), + }, + method: "POST", + query: {} as Record, + body: { + response_type: "code", + client_id: pending.clientId, + redirect_uri: pending.redirectUri, + scope: pending.scope, + state: pending.state, + code_challenge: pending.codeChallenge, + code_challenge_method: pending.codeChallengeMethod, + }, + }); + + const oauthRes = new OAuth2Server.Response({}); + + await oauth.authorize(oauthReq, oauthRes, { + authenticateHandler: { + handle: () => Promise.resolve({ id: userId, email }), + }, + }); + + const location = oauthRes.get("Location"); + if (!location) { + throw new Error( + "No redirect URI returned from authorize handler", + ); + } + + await OauthPendingAuth.deleteOne({ pendingId }); + + res.json({ success: true, redirectUri: location }); + } catch (err: any) { + logger.error({ error: err.message }, "Verify OTP error"); + res.json({ + success: false, + error: "Verification failed. Please try again.", + }); + } + }, +); + +oauthRouter.post( + "/oauth/token", + tokenLimiter, + async (req: ExpressReq, res: ExpressRes) => { + try { + const oauthReq = new OAuth2Server.Request({ + headers: { + "content-type": + req.headers["content-type"] || + "application/x-www-form-urlencoded", + ...Object.fromEntries( + Object.entries(req.headers).map(([k, v]) => [ + k, + String(v), + ]), + ), + }, + method: "POST", + query: {} as Record, + body: req.body || {}, + }); + + const oauthRes = new OAuth2Server.Response({}); + + await oauth.token(oauthReq, oauthRes); + + const body = (oauthRes as any).body; + if (!body) { + throw new Error("No response from token handler"); + } + + res.set(oauthRes.headers || {}); + res.status(oauthRes.status || 200).json(body); + } catch (err: any) { + logger.error({ error: err.message }, "Token exchange error"); + if (err instanceof OAuth2Server.OAuthError) { + res.status(err.code || 400).json({ + error: err.name, + error_description: err.message, + }); + } else { + res.status(500).json({ + error: "server_error", + error_description: "An unexpected error occurred.", + }); + } + } + }, +); + +oauthRouter.get( + "/oauth/userinfo", + userinfoLimiter, + async (req: ExpressReq, res: ExpressRes) => { + try { + const authParsed = z + .object({ + authorization: z + .string() + .regex( + /^Bearer\s+.+$/, + "Missing or invalid authorization header.", + ), + }) + .safeParse({ authorization: req.headers.authorization }); + + if (!authParsed.success) { + return res.status(401).json({ + error: "invalid_token", + error_description: authParsed.error.errors[0].message, + }); + } + const token = authParsed.data.authorization.substring(7); + const payload = verifyAccessToken(token); + if (!payload) { + return res.status(401).json({ + error: "invalid_token", + error_description: "Access token is invalid or expired.", + }); + } + const user = await getUser(payload.sub); + if (!user) { + return res.status(401).json({ + error: "invalid_token", + error_description: "User not found.", + }); + } + res.json({ + sub: String(user._id || user.id), + email: user.email, + name: user.name || "", + }); + } catch (err: any) { + logger.error({ error: err.message }, "UserInfo endpoint error"); + res.status(500).json({ + error: "server_error", + error_description: "An unexpected error occurred.", + }); + } + }, +); + +oauthRouter.post( + "/oauth/revoke", + revokeLimiter, + async (req: ExpressReq, res: ExpressRes) => { + try { + const { token } = req.body || {}; + if (token) { + const refreshToken = await oauthModel.getRefreshToken(token); + if (refreshToken) { + await oauthModel.revokeToken(refreshToken); + } + } + res.status(200).json({}); + } catch { + res.status(200).json({}); + } + }, +); + +oauthRouter.post( + "/oauth/register", + registerLimiter, + async (req: ExpressReq, res: ExpressRes) => { + try { + const meta = req.body as DcrRequest; + if ( + !meta.redirect_uris || + !Array.isArray(meta.redirect_uris) || + meta.redirect_uris.length === 0 + ) { + res.status(400).json({ + error: "invalid_redirect_uri", + error_description: "At least one redirect_uri is required.", + }); + return; + } + const client = await registerClient(meta); + res.status(201).json(client as DcrResponse); + } catch (err: any) { + if (err instanceof DcrValidationError) { + res.status(400).json({ + error: "invalid_client_metadata", + error_description: err.message, + }); + return; + } + logger.error({ error: err.message }, "DCR error"); + res.status(500).json({ + error: "server_error", + error_description: "Failed to register client.", + }); + } + }, +); diff --git a/apps/api/src/oauth/server.ts b/apps/api/src/oauth/server.ts new file mode 100644 index 00000000..db962221 --- /dev/null +++ b/apps/api/src/oauth/server.ts @@ -0,0 +1,18 @@ +import OAuth2Server from "@node-oauth/oauth2-server"; +import { oauthModel } from "./model"; + +export const oauth = new OAuth2Server({ + model: oauthModel, + allowEmptyState: true, + allowExtendedTokenAttributes: true, + accessTokenLifetime: Number(process.env.TOKEN_TTL_SECONDS) || 900, + refreshTokenLifetime: 60 * 60 * 24 * 30, // 30 days + authorizationCodeLifetime: 5 * 60, // 5 min + requireClientAuthentication: { + authorization_code: false, + refresh_token: false, + }, + alwaysIssueNewRefreshToken: true, +}); + +export { OAuth2Server }; diff --git a/apps/api/src/signature/routes.ts b/apps/api/src/signature/routes.ts index 1db200bd..561d433e 100644 --- a/apps/api/src/signature/routes.ts +++ b/apps/api/src/signature/routes.ts @@ -1,6 +1,7 @@ import express from "express"; import apikey from "../apikey/middleware"; import { getSignature } from "./handlers"; +import { authenticatedApiLimiter } from "../auth/limiters"; const router = express.Router(); router.post( @@ -9,7 +10,7 @@ router.post( #swagger.tags = ['Media'] #swagger.summary = 'Create Upload Signature' #swagger.description = 'Generate a signature for secure client-side uploads.' - #swagger.security = [{ "apiKeyAuth": [] }] + #swagger.security = [{ "bearerAuth": [] }, { "apiKeyAuth": [] }] #swagger.responses[200] = { description: 'Signature generated successfully', content: { @@ -24,6 +25,7 @@ router.post( #swagger.responses[401] = { description: 'Unauthorized' } #swagger.responses[500] = { description: 'Internal Server Error' } */ + authenticatedApiLimiter, apikey, getSignature, ); diff --git a/apps/api/src/swagger-generator.ts b/apps/api/src/swagger-generator.ts index 2a0ca685..7c55f009 100644 --- a/apps/api/src/swagger-generator.ts +++ b/apps/api/src/swagger-generator.ts @@ -65,6 +65,11 @@ const doc = { in: "header", name: "x-medialit-apikey", }, + bearerAuth: { + type: "http", + scheme: "bearer", + bearerFormat: "JWT", + }, signatureAuth: { type: "apiKey", in: "header", @@ -132,6 +137,15 @@ swaggerAutogen()(outputFile, routes, doc).then(() => { if (content.paths) { delete content.paths["/cleanup/temp"]; delete content.paths["/cleanup/tus"]; + delete content.paths["/mcp"]; + delete content.paths["/.well-known/oauth-authorization-server"]; + delete content.paths["/oauth/authorize"]; + delete content.paths["/oauth/authorize/send-otp"]; + delete content.paths["/oauth/authorize/verify-otp"]; + delete content.paths["/oauth/token"]; + delete content.paths["/oauth/revoke"]; + delete content.paths["/oauth/register"]; + delete content.paths["/oauth/userinfo"]; } Object.entries(content.paths || {}).forEach(([apiPath, pathItem]: any) => { diff --git a/apps/api/src/swagger_output.json b/apps/api/src/swagger_output.json index 22b0097e..a5d113ae 100644 --- a/apps/api/src/swagger_output.json +++ b/apps/api/src/swagger_output.json @@ -138,6 +138,9 @@ } }, "security": [ + { + "bearerAuth": [] + }, { "apiKeyAuth": [] } @@ -201,6 +204,9 @@ } }, "security": [ + { + "bearerAuth": [] + }, { "apiKeyAuth": [] } @@ -267,6 +273,9 @@ } }, "security": [ + { + "bearerAuth": [] + }, { "apiKeyAuth": [] } @@ -280,7 +289,7 @@ "Media" ], "summary": "Upload Media", - "description": "Upload a new media file. Use API key auth from Authorize (`x-medialit-apikey`) or pass `x-medialit-signature` for this endpoint only.", + "description": "Upload a new media file. Use OAuth Bearer auth, API key auth from Authorize (`x-medialit-apikey`), or pass `x-medialit-signature` for this endpoint only.", "parameters": [ { "name": "x-medialit-signature", @@ -357,6 +366,9 @@ } }, "security": [ + { + "bearerAuth": [] + }, { "apiKeyAuth": [] }, @@ -445,6 +457,9 @@ } }, "security": [ + { + "bearerAuth": [] + }, { "apiKeyAuth": [] } @@ -498,6 +513,9 @@ } }, "security": [ + { + "bearerAuth": [] + }, { "apiKeyAuth": [] } @@ -575,6 +593,9 @@ } }, "security": [ + { + "bearerAuth": [] + }, { "apiKeyAuth": [] } @@ -644,6 +665,9 @@ } }, "security": [ + { + "bearerAuth": [] + }, { "apiKeyAuth": [] } @@ -731,6 +755,9 @@ } }, "security": [ + { + "bearerAuth": [] + }, { "apiKeyAuth": [] } @@ -801,6 +828,9 @@ } }, "security": [ + { + "bearerAuth": [] + }, { "apiKeyAuth": [] } @@ -816,6 +846,11 @@ "in": "header", "name": "x-medialit-apikey" }, + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" + }, "signatureAuth": { "type": "apiKey", "in": "header", diff --git a/apps/api/src/tus/tus-server.ts b/apps/api/src/tus/tus-server.ts index e14342fa..c2402d1b 100644 --- a/apps/api/src/tus/tus-server.ts +++ b/apps/api/src/tus/tus-server.ts @@ -4,18 +4,12 @@ import { tempFileDirForUploads } from "../config/constants"; import logger from "../services/log"; import finalizeUpload from "./finalize"; import * as preSignedUrlService from "../signature/service"; -import { - FILE_SIZE_EXCEEDED, - NOT_ENOUGH_STORAGE, - PRESIGNED_URL_INVALID, - UNAUTHORISED, -} from "../config/strings"; +import { PRESIGNED_URL_INVALID, UNAUTHORISED } from "../config/strings"; import { Apikey, User } from "@medialit/models"; import { getApiKeyUsingKeyId } from "../apikey/queries"; import { getUser } from "../user/queries"; -import { hasEnoughStorage } from "../media/storage-middleware"; +import { validateUploadConstraints } from "../media/storage-middleware"; import { createTusUpload, updateTusUploadOffset } from "./queries"; -import getMaxFileUploadSize from "../media/utils/get-max-file-upload-size"; import mediaService from "../media/service"; const store = new FileStore({ @@ -41,45 +35,7 @@ export const server = new Server({ throw err; } }, - onUploadCreate: async (req: any, upload: any) => { - const metadata = upload.metadata; - const { user, apikey, group } = req; - - try { - const allowedFileSize = getMaxFileUploadSize(req); - if (upload.size > allowedFileSize) { - throw { - status_code: 403, - body: `${FILE_SIZE_EXCEEDED}. Allowed: ${allowedFileSize} bytes`, - }; - } - if (!(await hasEnoughStorage(upload.size, user))) { - throw { - status_code: 403, - body: NOT_ENOUGH_STORAGE, - }; - } - - await createTusUpload({ - uploadId: upload.id, - userId: user.id, - apikey: apikey!, - uploadLength: upload.size, - metadata: { - fileName: metadata.fileName || "unknown", - mimeType: metadata.mimeType || "application/octet-stream", - accessControl: metadata.access, - caption: metadata.caption, - group: metadata.group || group, - }, - tempFilePath: upload.id, - }); - } catch (err: any) { - logger.error({ err }, "Error creating tus upload record"); - throw err; - } - return metadata; - }, + onUploadCreate: handleTusUploadCreate, onUploadFinish: async (req: any, upload: any) => { try { console.time("finalize"); @@ -108,6 +64,43 @@ export const server = new Server({ }, }); +export async function handleTusUploadCreate(req: any, upload: any) { + const metadata = upload.metadata; + const { user, apikey, group } = req; + + try { + const validation = await validateUploadConstraints({ + size: upload.size, + user, + }); + if (!validation.valid) { + throw { + status_code: 403, + body: validation.error, + }; + } + + await createTusUpload({ + uploadId: upload.id, + userId: user.id, + apikey: apikey!, + uploadLength: upload.size, + metadata: { + fileName: metadata.fileName || "unknown", + mimeType: metadata.mimeType || "application/octet-stream", + accessControl: metadata.access, + caption: metadata.caption, + group: metadata.group || group, + }, + tempFilePath: upload.id, + }); + } catch (err: any) { + logger.error({ err }, "Error creating tus upload record"); + throw err; + } + return metadata; +} + server.on(EVENTS.POST_RECEIVE, async (req: any, upload: any) => { try { await updateTusUploadOffset(upload.id, upload.offset); diff --git a/apps/api/src/user/queries.ts b/apps/api/src/user/queries.ts index 33831ba5..9b611349 100644 --- a/apps/api/src/user/queries.ts +++ b/apps/api/src/user/queries.ts @@ -1,6 +1,7 @@ import { SubscriptionStatus, User } from "@medialit/models"; import UserModel from "./model"; import mongoose from "mongoose"; +import { createApiKey } from "../apikey/queries"; export async function getUser( id: string, @@ -19,10 +20,19 @@ export async function createUser( name?: string, subscriptionStatus?: SubscriptionStatus, ): Promise { - return await UserModel.create({ + const user = await UserModel.create({ email, active: true, name, subscriptionStatus, }); + + // Automatically create a default API key for the new user + await createApiKey( + String(user.id || (user as any)._id), + process.env.DEFAULT_APP_NAME || "My Store", + true, + ); + + return user; } diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json index 42bb5f39..22c7430c 100644 --- a/apps/api/tsconfig.json +++ b/apps/api/tsconfig.json @@ -2,12 +2,13 @@ "compilerOptions": { "outDir": "dist", "target": "es5", - "module": "commonjs", + "module": "nodenext", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, + "moduleResolution": "nodenext", "baseUrl": ".", "paths": { "@/*": [ diff --git a/apps/docs/content/docs/mcp-server.mdx b/apps/docs/content/docs/mcp-server.mdx new file mode 100644 index 00000000..5f2934d3 --- /dev/null +++ b/apps/docs/content/docs/mcp-server.mdx @@ -0,0 +1,177 @@ +--- +title: MCP Server +description: Control MediaLit using the Model Context Protocol (MCP) +icon: Terminal +--- + +## Introduction + +MediaLit includes a built-in [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server. Instead of forcing external AI coding assistants (like Claude Code, Cursor, or ChatGPT) to interact through raw REST endpoints, they can query the MCP server to directly discover and run high-level tools. + +The server uses the **Streamable HTTP** transport. Unlike standard stdio-based MCP servers that require running a separate subprocess, the MediaLit MCP server is mounted directly inside the main Express application at the `/mcp` route. This allows it to: +- Run on the same port and share the same SSL certificates. +- Reuse the application's authentication middlewares. +- Be accessible to both remote cloud-based clients (like ChatGPT) and local workspace assistants (like Claude Code). + +--- + +## How to Connect + +MediaLit supports two ways to authenticate your MCP client. **Using the OAuth 2.0 setup is recommended** as it is the easiest and most secure option. + +### 1. OAuth 2.0 Setup (Recommended) + +Connecting with OAuth is straightforward: +1. **Enter the Connection URL:** Provide the client with the MediaLit MCP server URL: `https://api.medialit.cloud/mcp` +2. **Redirect & Login:** The client will redirect you to the MediaLit login screen. +3. **Authorize:** Enter your email address to log in, and authorize the connection. + +Once approved, you are logged in and the client will automatically handle connection keys in the background. + + + When connecting via OAuth 2.0, the MCP server automatically associates the session with your **Default API Key**. Consequently, any files uploaded or managed via the MCP tools will be routed and stored under your **Default App**. + + + + +### 2. API Key Setup (Alternative) + +If you are using developer-focused tools like Cursor or Claude Code and prefer a quick configuration, you can use your MediaLit account API key instead. + +Add the following config to your client: +- **Header:** `x-medialit-apikey` +- **Value:** `YOUR_API_KEY` + +**Claude Desktop Configuration (`claude_desktop_config.json`):** + +Since Claude Desktop communicates via standard input/output (stdio), you must use a proxy/bridge tool like `mcp-remote` to connect to the remote Streamable HTTP server: + +```json +{ + "mcpServers": { + "medialit": { + "command": "npx", + "args": [ + "mcp-remote", + "https://api.medialit.cloud/mcp", + "--header", + "x-medialit-apikey: ${API_KEY}" + ], + "env": { + "API_KEY": "YOUR_API_KEY" + } + } + } +} +``` + +**VS Code configuration (`mcp.json`):** +``` +{ + "servers": { + "medialit": { + "type": "http", + "url": "https://api.medialit.cloud/mcp", + "headers": { + "x-medialit-apikey": "YOUR_API_KEY" + } + } + } +} +``` + +--- + +## Two-Step Upload Flow (Drafts & Sealing) + +To prevent cluttering of the media registry with unfinished uploads, MediaLit enforces a **two-step upload pattern** across both REST and MCP interfaces: + +1. **Upload (`upload_media`):** The file is written to storage, and a record is created with `temp: true`. In this temporary state, the file does **not** appear in paginated listings (`list_media`), count queries (`get_media_count`), or size tallies (`get_media_size`). +2. **Seal (`seal_media`):** The client verifies the upload and seals it. Sealing removes the `temp` flag, finalizing the record and making it globally visible to listing tools. + + + Files left in the `temp: true` state for more than 24 hours are automatically purged by a background cleanup worker. Always ensure you call `seal_media` once the upload is completed successfully. + + +--- + +## Tools Reference + +The MediaLit MCP server exposes the following tools to connected agents: + +### Media Management + +#### `list_media` +Returns a paginated list of finalized (sealed) media items. +- **Inputs:** + - `page` *(number, optional)*: Page offset (default: `1`). + - `limit` *(number, optional)*: Items per page (default: `10`). + - `access` *("public" | "private", optional)*: Filter by accessibility level. + - `group` *(string, optional)*: Filter by custom group label. +- **Output:** `{ mediaItems: Array, total: number, page: number }` + +#### `get_media` +Fetches complete metadata for a single media item by its ID. +- **Inputs:** + - `mediaId` *(string, required)*: The unique ID of the media item. +- **Output:** The media document. +- **Note:** Unlike `list_media`, this tool can fetch `temp: true` (unsealed) media items, allowing validation before sealing. + +#### `get_media_count` +Returns the total count of finalized media items in the account. +- **Output:** `{ count: number }` + +#### `get_media_size` +Returns the total storage space consumed by the account, along with the allowed maximum limit in bytes. +- **Output:** `{ storage: { storage: number, maxStorage: number } }` + +#### `seal_media` +Finalizes a temporary media upload, allowing it to persist and show up in lists/tallies. +- **Inputs:** + - `mediaId` *(string, required)*: The unique ID of the media item to seal. +- **Output:** The updated media document. + +#### `delete_media` +Permanently deletes a media file and all generated derivatives (thumbnails, WebP conversions) from storage. +- **Inputs:** + - `mediaId` *(string, required)*: The unique ID of the media item to delete. +- **Output:** `{ deleted: true, mediaId: string }` + +--- + +### File Uploading & Access + +#### `upload_media` +Uploads a new file directly using base64-encoded payload parameters. +- **Inputs:** + - `fileBase64` *(string, required)*: Base64-encoded string of the file bytes. + - `fileName` *(string, required)*: Filename with extension (e.g. `image.png`). + - `mimeType` *(string, required)*: The MIME type of the file (e.g. `image/png`). + - `caption` *(string, optional)*: Text description or caption. + - `access` *("public" | "private", optional)*: Access controls (default: `private`). + - `group` *(string, optional)*: Custom folder/group tag. +- **Output:** `{ mediaId: string }` +- **Limits:** Maximum payload size is restricted to **10MB**. For larger files, generate a signature and use the TUS protocol via REST. + +#### `create_upload_signature` +Generates a time-limited HMAC signature to authorize direct browser-to-bucket uploads. +- **Inputs:** + - `group` *(string, optional)*: Optional group to associate with the uploaded file. +- **Output:** `{ signature: string }` + +--- + +### Media Settings + +#### `get_media_settings` +Retrieves the active image processing configurations for the account, such as auto-conversion behaviors. +- **Output:** Active configurations (WebP defaults, thumbnail dimensions). + +#### `update_media_settings` +Overwrites specific image processing defaults. Supply only the properties you wish to modify. +- **Inputs:** + - `useWebP` *(boolean, optional)*: Auto-convert uploaded images to WebP. + - `webpOutputQuality` *(number, optional)*: Output quality between `0` and `100`. + - `thumbnailWidth` *(number, optional)*: Default width of generated thumbnails. + - `thumbnailHeight` *(number, optional)*: Default height of generated thumbnails. +- **Output:** `{ updated: true }` diff --git a/apps/docs/content/docs/meta.json b/apps/docs/content/docs/meta.json index 47789a7a..0e6cd906 100644 --- a/apps/docs/content/docs/meta.json +++ b/apps/docs/content/docs/meta.json @@ -9,6 +9,8 @@ "express-js", "---API Endpoints---", "api", + "---MCP Server---", + "mcp-server", "---Resources---", "self-hosting", "migrate-to-dual-bucket-architecture", diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile index 1fdfa1d8..c2b64dd5 100644 --- a/apps/web/Dockerfile +++ b/apps/web/Dockerfile @@ -19,8 +19,8 @@ COPY apps/web/lib ./apps/web/lib COPY apps/web/models ./apps/web/models COPY apps/web/public ./apps/web/public COPY apps/web/utils ./apps/web/utils -COPY apps/web/auth.config.ts ./apps/web/auth.config.ts COPY apps/web/auth.ts ./apps/web/auth.ts +COPY apps/web/middleware.ts ./apps/web/middleware.ts COPY apps/web/components.json ./apps/web/components.json COPY apps/web/next.config.js ./apps/web/next.config.js COPY apps/web/postcss.config.js ./apps/web/postcss.config.js diff --git a/apps/web/app/account/billing/action.ts b/apps/web/app/account/billing/action.ts index 1e82af87..c74ee6aa 100644 --- a/apps/web/app/account/billing/action.ts +++ b/apps/web/app/account/billing/action.ts @@ -1,6 +1,6 @@ "use server"; -import { Session } from "next-auth"; +import { Session } from "@/auth"; import connectToDatabase from "@/lib/connect-db"; import UserModel from "@/models/user"; import { LEMONSQUEEZY_API_KEY } from "@/lib/constants"; diff --git a/apps/web/app/account/billing/lemonsqueezy-start-subscription-button.tsx b/apps/web/app/account/billing/lemonsqueezy-start-subscription-button.tsx index 168a5259..529412a3 100644 --- a/apps/web/app/account/billing/lemonsqueezy-start-subscription-button.tsx +++ b/apps/web/app/account/billing/lemonsqueezy-start-subscription-button.tsx @@ -3,7 +3,7 @@ import { ReactNode, useEffect, useMemo, useState } from "react"; import { Button } from "../../../components/ui/button"; import { useRouter } from "next/navigation"; -import { Session } from "next-auth"; +import { Session } from "@/auth"; import { useToast } from "../../../components/ui/use-toast"; export default function LemonSqueezyStartSubscriptionButton({ diff --git a/apps/web/app/actions.ts b/apps/web/app/actions.ts index 934deaa2..513b7bf5 100644 --- a/apps/web/app/actions.ts +++ b/apps/web/app/actions.ts @@ -1,12 +1,7 @@ "use server"; -import { AuthError, Session } from "next-auth"; -import { createTransport } from "nodemailer"; -import { auth, signIn } from "@/auth"; -import { SITE_NAME } from "@/lib/constants"; -import { generateUniquePasscode, hashCode } from "@/lib/magic-code-utils"; +import { auth, Session } from "@/auth"; import connectToDatabase from "@/lib/connect-db"; -import verificationToken from "@/models/verification-token"; import { createApiKey, getApiKeysByUserId, @@ -19,87 +14,6 @@ import { Apikey } from "@medialit/models"; import UserModel from "@/models/user"; import { User } from "@medialit/models"; -export async function authenticate( - prevState: Record, - formData: FormData, -): Promise<{ - success: boolean; - checked: boolean; - error?: string; -}> { - try { - await signIn("credentials", { - email: formData.get("email"), - code: formData.get("code"), - redirect: false, - }); - return { success: true, checked: true }; - } catch (error) { - if (error instanceof AuthError) { - switch (error.type) { - case "CredentialsSignin": - return { - success: false, - checked: true, - error: "Invalid credentials", - }; - default: - return { - success: false, - checked: true, - error: "Something went wrong", - }; - } - } - return { success: false, checked: true, error: (error as any).message }; - } -} - -export async function sendCode( - prevState: Record, - formData: FormData, -): Promise<{ success: boolean; error?: string }> { - const email = formData.get("email") as string; - const code = generateUniquePasscode(); - await connectToDatabase(); - - await verificationToken.create({ - email, - code: hashCode(code), - timestamp: Date.now() + 1000 * 60 * 5, - }); - - if (process.env.NODE_ENV !== "production") { - console.log("Sending email to", email, "with code", code); - return { success: true }; - } - - const transporter = createTransport({ - host: process.env.EMAIL_HOST, - port: +(process.env.EMAIL_PORT || 587), - auth: { - user: process.env.EMAIL_USER, - pass: process.env.EMAIL_PASS, - }, - }); - - try { - await transporter.sendMail({ - from: process.env.EMAIL_FROM, - to: email, - subject: `Your verification code for ${SITE_NAME}`, - html: ` - Enter the following code in to the app. - ${code} - `, - }); - - return { success: true }; - } catch (err: any) { - return { success: false, error: err.message }; - } -} - export async function getUser(): Promise { const session: Session | null = await auth(); if (!session || !session.user) { @@ -156,7 +70,7 @@ export async function getApiKeys() { export async function getApikeyUsingKeyId( keyId: string, -): Promise | null> { +): Promise | null> { const session = await auth(); if (!session || !session.user) { throw new Error("Unauthenticated"); @@ -179,6 +93,7 @@ export async function getApikeyUsingKeyId( keyId: apikey.keyId, name: apikey.name, key: apikey.key, + default: apikey.default, }; } @@ -234,6 +149,16 @@ export async function deleteApiKeyOfUser( return { success: false, error: "Invalid User" }; } + const apikey = await getApikeyFromKeyId(dbUser._id, keyId); + + if (!apikey) { + return { success: false, error: "Apikey not found" }; + } + + if (apikey.default) { + return { success: false, error: "Default Apikey cannot be deleted" }; + } + try { const result = await deleteApiKey(dbUser._id, keyId); return { success: true }; diff --git a/apps/web/app/api/auth/[...nextauth].ts b/apps/web/app/api/auth/[...nextauth].ts deleted file mode 100644 index 86c9f3da..00000000 --- a/apps/web/app/api/auth/[...nextauth].ts +++ /dev/null @@ -1,3 +0,0 @@ -import { handlers } from "@/auth"; - -export const { GET, POST } = handlers; diff --git a/apps/web/app/api/auth/callback/medialit/route.ts b/apps/web/app/api/auth/callback/medialit/route.ts new file mode 100644 index 00000000..7507e5f5 --- /dev/null +++ b/apps/web/app/api/auth/callback/medialit/route.ts @@ -0,0 +1,114 @@ +import { NextRequest, NextResponse } from "next/server"; +import { cookies } from "next/headers"; +import { + ACCESS_TOKEN_COOKIE, + DEFAULT_ACCESS_TOKEN_MAX_AGE_SECONDS, + REFRESH_TOKEN_COOKIE, + REFRESH_TOKEN_MAX_AGE_SECONDS, + USER_COOKIE, + tokenCookieOptions, +} from "@/lib/oauth-session"; + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + const code = searchParams.get("code"); + + const cookieStore = await cookies(); + const codeVerifier = cookieStore.get("oauth_code_verifier")?.value; + + if (!code || !codeVerifier) { + return new NextResponse("Missing authorization code or verifier", { + status: 400, + }); + } + + const origin = process.env.WEB_CLIENT || "http://localhost:3000"; + const redirectUri = `${origin}/api/auth/callback/medialit`; + + try { + const tokenResponse = await fetch( + `${process.env.API_SERVER}/oauth/token`, + { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ + grant_type: "authorization_code", + code, + redirect_uri: redirectUri, + client_id: "web-client", + code_verifier: codeVerifier, + }).toString(), + }, + ); + + if (!tokenResponse.ok) { + const errBody = await tokenResponse.text(); + console.error("Token exchange failed:", errBody); + return new NextResponse("Token exchange failed", { status: 400 }); + } + + const tokenData = await tokenResponse.json(); + const accessToken = tokenData.access_token; + const refreshToken = tokenData.refresh_token; + + if (!accessToken || !refreshToken) { + return new NextResponse("No access or refresh token returned", { + status: 400, + }); + } + + const userinfoResponse = await fetch( + `${process.env.API_SERVER}/oauth/userinfo`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + if (!userinfoResponse.ok) { + console.error("Failed to fetch UserInfo"); + return new NextResponse("UserInfo lookup failed", { status: 400 }); + } + + const userData = await userinfoResponse.json(); + + cookieStore.set( + ACCESS_TOKEN_COOKIE, + accessToken, + tokenCookieOptions( + Number(tokenData.expires_in) || + DEFAULT_ACCESS_TOKEN_MAX_AGE_SECONDS, + ), + ); + cookieStore.set( + REFRESH_TOKEN_COOKIE, + refreshToken, + tokenCookieOptions(REFRESH_TOKEN_MAX_AGE_SECONDS), + ); + + cookieStore.set( + USER_COOKIE, + JSON.stringify({ + id: userData.sub, + email: userData.email, + name: userData.name, + }), + { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + path: "/", + }, + ); + + cookieStore.delete("oauth_code_verifier"); + + return NextResponse.redirect(new URL("/", redirectUri)); + } catch (error: any) { + console.error("OAuth callback error:", error); + return new NextResponse("Internal server error", { status: 500 }); + } +} diff --git a/apps/web/app/api/auth/signout/route.ts b/apps/web/app/api/auth/signout/route.ts new file mode 100644 index 00000000..dc00865c --- /dev/null +++ b/apps/web/app/api/auth/signout/route.ts @@ -0,0 +1,25 @@ +import { NextRequest, NextResponse } from "next/server"; +import { cookies } from "next/headers"; +import { + ACCESS_TOKEN_COOKIE, + REFRESH_TOKEN_COOKIE, + USER_COOKIE, + revokeRefreshToken, +} from "@/lib/oauth-session"; + +export async function GET(request: NextRequest) { + const cookieStore = await cookies(); + await revokeRefreshToken(cookieStore.get(REFRESH_TOKEN_COOKIE)?.value); + cookieStore.delete(ACCESS_TOKEN_COOKIE); + cookieStore.delete(REFRESH_TOKEN_COOKIE); + cookieStore.delete(USER_COOKIE); + return NextResponse.redirect(new URL("/login", request.url)); +} +export async function POST(request: NextRequest) { + const cookieStore = await cookies(); + await revokeRefreshToken(cookieStore.get(REFRESH_TOKEN_COOKIE)?.value); + cookieStore.delete(ACCESS_TOKEN_COOKIE); + cookieStore.delete(REFRESH_TOKEN_COOKIE); + cookieStore.delete(USER_COOKIE); + return NextResponse.redirect(new URL("/login", request.url)); +} diff --git a/apps/web/app/login/page.tsx b/apps/web/app/login/page.tsx deleted file mode 100644 index 2a3b7857..00000000 --- a/apps/web/app/login/page.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { auth } from "@/auth"; -import LoginForm from "@/components/login-form"; -import Link from "next/link"; -import { redirect } from "next/navigation"; - -export default async function LoginPage() { - const session = await auth(); - - if (session) { - redirect("/"); - } - - return ( -
-

Sign in

- -

- By signing in, you agree to our{" "} - - Terms - {" "} - and{" "} - - Privacy policy - - . -

-
- ); -} diff --git a/apps/web/app/login/route.ts b/apps/web/app/login/route.ts new file mode 100644 index 00000000..fa15a8d1 --- /dev/null +++ b/apps/web/app/login/route.ts @@ -0,0 +1,39 @@ +import { cookies } from "next/headers"; +import { NextResponse } from "next/server"; +import crypto from "crypto"; + +export async function GET(request: Request) { + const cookieStore = await cookies(); + + const state = crypto.randomBytes(16).toString("hex"); + const codeVerifier = crypto.randomBytes(32).toString("base64url"); + + const codeChallenge = crypto + .createHash("sha256") + .update(codeVerifier) + .digest("base64url"); + + cookieStore.set("oauth_code_verifier", codeVerifier, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + maxAge: 600, // 10 minutes + path: "/", + }); + + const origin = process.env.WEB_CLIENT || "http://localhost:3000"; + const redirectUri = `${origin}/api/auth/callback/medialit`; + + const authUrl = + `${process.env.API_SERVER}/oauth/authorize?` + + new URLSearchParams({ + response_type: "code", + client_id: "web-client", + redirect_uri: redirectUri, + code_challenge: codeChallenge, + code_challenge_method: "S256", + state: state, + }).toString(); + + return NextResponse.redirect(authUrl); +} diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 5bf0a9b8..a6fd0f83 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -6,6 +6,7 @@ import NewApp from "@/components/new-app-button"; import { FolderPlus } from "lucide-react"; import { Card, CardContent } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; +import { Badge } from "@/components/ui/badge"; import { getTotalSpaceByApikey } from "./app/[keyid]/settings/actions"; export default async function Home() { @@ -50,11 +51,19 @@ export default async function Home() { href={`/app/${apikey.keyId}/files`} key={apikey.keyId} > - + {apikey.name || "Untitled"} + {apikey.default && ( + + Default + + )} diff --git a/apps/web/auth.config.ts b/apps/web/auth.config.ts deleted file mode 100644 index a822d223..00000000 --- a/apps/web/auth.config.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { type NextAuthConfig } from "next-auth"; - -export const authConfig = { - debug: true, - pages: { - signIn: "/login", - }, - callbacks: { - async redirect({ url, baseUrl }: any) { - return baseUrl; - }, - }, - - providers: [], // Add providers with an empty array for now -} satisfies NextAuthConfig; diff --git a/apps/web/auth.ts b/apps/web/auth.ts index 152d8165..ae316063 100644 --- a/apps/web/auth.ts +++ b/apps/web/auth.ts @@ -1,72 +1,50 @@ -import NextAuth from "next-auth"; -import { z } from "zod"; -import { authConfig } from "./auth.config"; -import { hashCode } from "@/lib/magic-code-utils"; -import { Constants } from "@medialit/models"; -import { getUniqueId } from "@medialit/utils"; -import CredentialsProvider from "next-auth/providers/credentials"; -import connectToDatabase from "@/lib/connect-db"; -import VerificationToken from "@/models/verification-token"; -import User from "@/models/user"; -import Apikey from "@/models/apikey"; -import { createUser } from "./lib/courselit"; +"use server"; -export const { auth, signIn, signOut, handlers } = NextAuth({ - ...authConfig, - providers: [ - CredentialsProvider({ - name: "Email", - credentials: {}, - async authorize(credentials, req) { - const parsedCredentials = z - .object({ - email: z.string().email(), - code: z.string().min(6), - }) - .safeParse(credentials); - if (!parsedCredentials.success) { - return null; - } +import { cookies } from "next/headers"; +import { redirect } from "next/navigation"; +import { + ACCESS_TOKEN_COOKIE, + REFRESH_TOKEN_COOKIE, + USER_COOKIE, + revokeRefreshToken, +} from "@/lib/oauth-session"; - const { email, code }: any = parsedCredentials.data; - const sanitizedEmail = email.toLowerCase(); +export interface SessionUser { + id: string; + email: string; + name?: string; +} - await connectToDatabase(); - const verificationToken = - await VerificationToken.findOneAndDelete({ - email: sanitizedEmail, - code: hashCode(code), - timestamp: { $gt: Date.now() }, - }); +export interface Session { + user: SessionUser; + accessToken: string; +} - if (!verificationToken) { - return null; - } +export async function auth(): Promise { + const cookieStore = await cookies(); + const accessToken = cookieStore.get(ACCESS_TOKEN_COOKIE)?.value; + const userJson = cookieStore.get(USER_COOKIE)?.value; - let user = await User.findOne({ - email: sanitizedEmail, - }); + if (!accessToken || !userJson) { + return null; + } - if (!user) { - user = await User.create({ - email: sanitizedEmail, - active: true, - subscriptionStatus: - Constants.SubscriptionStatus.NOT_SUBSCRIBED, - }); - try { - await createUser({ email: sanitizedEmail }); - } catch (err: any) { - console.error("Error creating user in CourseLit"); - console.error(err); - } - } - return { - id: user.userId, - email: sanitizedEmail, - name: user.name, - }; - }, - }), - ], -}); + try { + const user = JSON.parse(userJson) as SessionUser; + return { + user, + accessToken, + }; + } catch { + return null; + } +} + +export async function signOut() { + const cookieStore = await cookies(); + await revokeRefreshToken(cookieStore.get(REFRESH_TOKEN_COOKIE)?.value); + cookieStore.delete(ACCESS_TOKEN_COOKIE); + cookieStore.delete(REFRESH_TOKEN_COOKIE); + cookieStore.delete(USER_COOKIE); + redirect("/login"); +} diff --git a/apps/web/components/auth-button.tsx b/apps/web/components/auth-button.tsx deleted file mode 100644 index c3012770..00000000 --- a/apps/web/components/auth-button.tsx +++ /dev/null @@ -1,24 +0,0 @@ -"use client"; - -import Button from "./button"; -import { signIn, signOut, useSession } from "next-auth/react"; - -export default function AuthButton() { - const { data: session } = useSession(); - if (session) { - return ( -
- -
- ); - } - return ( -
- -
- ); -} diff --git a/apps/web/components/login-form.tsx b/apps/web/components/login-form.tsx deleted file mode 100644 index 903e6306..00000000 --- a/apps/web/components/login-form.tsx +++ /dev/null @@ -1,97 +0,0 @@ -"use client"; - -import { useFormStatus } from "react-dom"; -import { authenticate, sendCode } from "../app/actions"; -import { useState, useActionState } from "react"; -import { Button } from "./ui/button"; -import { Input } from "./ui/input"; -import { redirect, useRouter } from "next/navigation"; - -export default function LoginForm() { - const [codeFormState, sendCodeFormAction] = useActionState(sendCode, { - success: false, - }); - const [verifyFormState, verifyCodeFormAction] = useActionState( - authenticate, - { - success: false, - checked: false, - }, - ); - const [email, setEmail] = useState(""); - const [code, setCode] = useState(""); - const router = useRouter(); - - if (verifyFormState.checked && verifyFormState.success) { - router.refresh(); - redirect("/"); - } - - return ( - <> - {codeFormState.success && ( -
- {verifyFormState.error && ( -

- Can't sign you in at this moment. Reason:{" "} - {verifyFormState.error}. -

- )} - - setCode(e.target.value)} - value={code} - minLength={6} - /> - Login -
- )} - {!codeFormState.success && ( -
- {codeFormState.error && ( -

{codeFormState.error}

- )} - setEmail(e.target.value)} - required - /> - Get code -
- )} - - ); -} - -function Submit({ children }: { children: React.ReactNode }) { - const status = useFormStatus(); - - return ( - - ); -} diff --git a/apps/web/components/ui/badge.tsx b/apps/web/components/ui/badge.tsx new file mode 100644 index 00000000..2353fc57 --- /dev/null +++ b/apps/web/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ); +} + +export { Badge, badgeVariants }; diff --git a/apps/web/lib/apikey-handlers.ts b/apps/web/lib/apikey-handlers.ts index 0ebacc20..31f862af 100644 --- a/apps/web/lib/apikey-handlers.ts +++ b/apps/web/lib/apikey-handlers.ts @@ -14,6 +14,7 @@ export async function getApiKeysByUserId( // key: 1, httpReferrers: 1, ipAddresses: 1, + default: 1, createdAt: 1, updatedAt: 1, keyId: 1, @@ -57,6 +58,10 @@ export async function deleteApiKey( userId: mongoose.Types.ObjectId, keyId: string, ) { + const key = await ApikeyModel.findOne({ keyId, userId }); + if (key && key.default) { + throw new Error("Default API key cannot be deleted"); + } return await ApikeyModel.updateOne( { keyId, diff --git a/apps/web/lib/magic-code-utils.ts b/apps/web/lib/magic-code-utils.ts deleted file mode 100644 index 9ef51934..00000000 --- a/apps/web/lib/magic-code-utils.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { createHash, randomInt } from "crypto"; - -export function generateUniquePasscode() { - return randomInt(100000, 999999); -} - -// Inspired from: https://github.com/nextauthjs/next-auth/blob/c4ad77b86762b7fd2e6362d8bf26c5953846774a/packages/next-auth/src/core/lib/utils.ts#L16 -export function hashCode(code: number) { - return createHash("sha256") - .update(`${code}${process.env.NEXTAUTH_SECRET}`) - .digest("hex"); -} diff --git a/apps/web/lib/oauth-session.ts b/apps/web/lib/oauth-session.ts new file mode 100644 index 00000000..a85117f9 --- /dev/null +++ b/apps/web/lib/oauth-session.ts @@ -0,0 +1,72 @@ +export const ACCESS_TOKEN_COOKIE = "session_access_token"; +export const REFRESH_TOKEN_COOKIE = "session_refresh_token"; +export const USER_COOKIE = "session_user"; + +export const ACCESS_TOKEN_REFRESH_SKEW_SECONDS = 60; +export const DEFAULT_ACCESS_TOKEN_MAX_AGE_SECONDS = 15 * 60; +export const REFRESH_TOKEN_MAX_AGE_SECONDS = 30 * 24 * 60 * 60; + +type TokenCookieOptions = { + httpOnly: boolean; + secure: boolean; + sameSite: "lax"; + path: string; + maxAge: number; +}; + +export function tokenCookieOptions(maxAge: number): TokenCookieOptions { + return { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + path: "/", + maxAge, + }; +} + +function decodeBase64Url(value: string): string { + const base64 = value.replace(/-/g, "+").replace(/_/g, "/"); + const padded = base64.padEnd( + base64.length + ((4 - (base64.length % 4)) % 4), + "=", + ); + + return atob(padded); +} + +export function getJwtExpiresAtSeconds(token: string): number | null { + const [, payload] = token.split("."); + if (!payload) return null; + + try { + const parsed = JSON.parse(decodeBase64Url(payload)); + return typeof parsed.exp === "number" ? parsed.exp : null; + } catch { + return null; + } +} + +export function shouldRefreshAccessToken( + token: string, + nowSeconds = Math.floor(Date.now() / 1000), +): boolean { + const expiresAt = getJwtExpiresAtSeconds(token); + if (!expiresAt) return true; + return expiresAt - nowSeconds <= ACCESS_TOKEN_REFRESH_SKEW_SECONDS; +} + +export async function revokeRefreshToken(refreshToken?: string): Promise { + if (!refreshToken || !process.env.API_SERVER) return; + + try { + await fetch(`${process.env.API_SERVER}/oauth/revoke`, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ token: refreshToken }).toString(), + }); + } catch { + return; + } +} diff --git a/apps/web/lib/user-handlers.ts b/apps/web/lib/user-handlers.ts index 17b93b80..e20e9df1 100644 --- a/apps/web/lib/user-handlers.ts +++ b/apps/web/lib/user-handlers.ts @@ -1,16 +1,15 @@ import UserModel from "@/models/user"; import { User } from "@medialit/models"; import mongoose from "mongoose"; -import { Session } from "next-auth"; type UserWithId = User & { _id: mongoose.Types.ObjectId }; export async function getUserFromSession( - session: Session, + session: { user?: { email?: string | null } } | null, ): Promise { - const { user } = session; + if (!session || !session.user || !session.user.email) return null; const dbUser: UserWithId | null = (await UserModel.findOne({ - email: user!.email, + email: session.user.email, }).lean()) as UserWithId | null; return dbUser; diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts new file mode 100644 index 00000000..0f0724ec --- /dev/null +++ b/apps/web/middleware.ts @@ -0,0 +1,148 @@ +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; +import { + ACCESS_TOKEN_COOKIE, + DEFAULT_ACCESS_TOKEN_MAX_AGE_SECONDS, + REFRESH_TOKEN_COOKIE, + REFRESH_TOKEN_MAX_AGE_SECONDS, + USER_COOKIE, + shouldRefreshAccessToken, + tokenCookieOptions, +} from "@/lib/oauth-session"; + +async function refreshSession(request: NextRequest) { + const refreshToken = request.cookies.get(REFRESH_TOKEN_COOKIE)?.value; + if (!refreshToken || !process.env.API_SERVER) return null; + + try { + const tokenResponse = await fetch( + `${process.env.API_SERVER}/oauth/token`, + { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ + grant_type: "refresh_token", + refresh_token: refreshToken, + client_id: "web-client", + }).toString(), + }, + ); + + if (!tokenResponse.ok) return null; + + const tokenData = await tokenResponse.json(); + if (!tokenData.access_token || !tokenData.refresh_token) return null; + + return { + accessToken: String(tokenData.access_token), + refreshToken: String(tokenData.refresh_token), + expiresIn: + Number(tokenData.expires_in) || + DEFAULT_ACCESS_TOKEN_MAX_AGE_SECONDS, + }; + } catch { + return null; + } +} + +function redirectToLogin(request: NextRequest) { + const url = request.nextUrl.clone(); + url.pathname = "/login"; + const response = NextResponse.redirect(url); + response.cookies.delete(ACCESS_TOKEN_COOKIE); + response.cookies.delete(REFRESH_TOKEN_COOKIE); + response.cookies.delete(USER_COOKIE); + return response; +} + +function setRefreshedTokenCookies( + response: NextResponse, + refreshed: { + accessToken: string; + refreshToken: string; + expiresIn: number; + }, +) { + response.cookies.set( + ACCESS_TOKEN_COOKIE, + refreshed.accessToken, + tokenCookieOptions(refreshed.expiresIn), + ); + response.cookies.set( + REFRESH_TOKEN_COOKIE, + refreshed.refreshToken, + tokenCookieOptions(REFRESH_TOKEN_MAX_AGE_SECONDS), + ); +} + +export async function middleware(request: NextRequest) { + const accessToken = request.cookies.get(ACCESS_TOKEN_COOKIE)?.value; + const refreshToken = request.cookies.get(REFRESH_TOKEN_COOKIE)?.value; + const url = request.nextUrl.clone(); + + const isLoginPage = url.pathname.startsWith("/login"); + const isCallbackPage = url.pathname.startsWith( + "/api/auth/callback/medialit", + ); + const isSignoutPage = url.pathname.startsWith("/api/auth/signout"); + const isStaticAsset = + url.pathname.includes(".") || + url.pathname.startsWith("/_next") || + url.pathname.startsWith("/api/cleanup"); + + if (isStaticAsset) { + return NextResponse.next(); + } + + if (!accessToken && !isCallbackPage && !isSignoutPage) { + if (refreshToken) { + const refreshed = await refreshSession(request); + if (refreshed) { + if (isLoginPage) { + url.pathname = "/"; + const response = NextResponse.redirect(url); + setRefreshedTokenCookies(response, refreshed); + return response; + } + + const response = NextResponse.next(); + setRefreshedTokenCookies(response, refreshed); + return response; + } + } + + if (!isLoginPage) { + return redirectToLogin(request); + } + } + + if (accessToken && isLoginPage) { + url.pathname = "/"; + return NextResponse.redirect(url); + } + + if ( + accessToken && + !isLoginPage && + !isCallbackPage && + !isSignoutPage && + shouldRefreshAccessToken(accessToken) + ) { + const refreshed = await refreshSession(request); + if (!refreshed) return redirectToLogin(request); + + const response = NextResponse.next(); + setRefreshedTokenCookies(response, refreshed); + return response; + } + + return NextResponse.next(); +} + +export const config = { + matcher: [ + "/((?!api/cleanup|_next/static|_next/image|favicon.ico|icon.svg).*)", + ], +}; diff --git a/apps/web/models/verification-token.ts b/apps/web/models/verification-token.ts deleted file mode 100644 index ae0d1a8c..00000000 --- a/apps/web/models/verification-token.ts +++ /dev/null @@ -1,25 +0,0 @@ -import mongoose from "mongoose"; - -export interface VerificationToken { - _id: mongoose.Types.ObjectId; - email: string; - code: string; - timestamp: Date; -} - -const VerificationTokenSchema = new mongoose.Schema({ - email: { type: String, required: true }, - code: { type: String, required: true }, - timestamp: { type: Date, required: true }, -}); - -VerificationTokenSchema.index( - { - email: 1, - code: 1, - }, - { unique: true }, -); - -export default mongoose.models?.VerificationToken || - mongoose.model("VerificationToken", VerificationTokenSchema); diff --git a/apps/web/next.config.js b/apps/web/next.config.js index 3a186bf2..58f1cb58 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -24,7 +24,7 @@ const nextConfig = { }, { protocol: "https", - hostname: "cdn.medialit.clqa.online", + hostname: "cdn.medialit.clqa.site", }, { protocol: "https", diff --git a/apps/web/package-lock.json b/apps/web/package-lock.json deleted file mode 100644 index 6cc4e235..00000000 --- a/apps/web/package-lock.json +++ /dev/null @@ -1,8587 +0,0 @@ -{ - "name": "@medialit/web", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@medialit/web", - "version": "0.1.0", - "dependencies": { - "@radix-ui/colors": "^3.0.0", - "@radix-ui/react-avatar": "^1.0.4", - "@radix-ui/react-dialog": "^1.0.5", - "@radix-ui/react-dropdown-menu": "^2.0.6", - "@radix-ui/react-icons": "^1.3.0", - "@radix-ui/react-label": "^2.0.2", - "@radix-ui/react-popover": "^1.0.7", - "@radix-ui/react-progress": "^1.1.4", - "@radix-ui/react-separator": "^1.0.3", - "@radix-ui/react-slot": "^1.2.0", - "@radix-ui/react-switch": "^1.0.3", - "@radix-ui/react-tabs": "^1.1.8", - "@radix-ui/react-toast": "^1.1.5", - "class-variance-authority": "^0.7.0", - "clsx": "^2.1.0", - "lucide-react": "^0.487.0", - "mongoose": "^8.0.1", - "next": "^15.3.1", - "next-auth": "5.0.0-beta.25", - "next-themes": "^0.2.1", - "nodemailer": "^6.7.2", - "react": "19.1.0", - "react-dom": "19.1.0", - "react-icons": "^4.11.0", - "sonner": "^1.3.1", - "tailwind-merge": "^2.2.0", - "tailwindcss-animate": "^1.0.7", - "zod": "^3.22.4" - }, - "devDependencies": { - "@types/json-schema": "^7.0.15", - "@types/node": "20.10.6", - "@types/nodemailer": "^6.4.13", - "@types/react": "19.1.0", - "@types/react-dom": "19.1.1", - "autoprefixer": "^10.4.17", - "eslint": "^8", - "eslint-config-next": "15.2.4", - "postcss": "^8.4.33", - "tailwindcss": "^3.4.1", - "typescript": "^5" - } - }, - "../../node_modules/.pnpm/@courselit+components-library@0.38.1_@codemirror+view@6.22.0_@emotion+react@11.11.1_@lezer+co_66gczrwkzldn4hwddjrbwqxtrq/node_modules/@courselit/components-library": { - "version": "0.38.1", - "extraneous": true, - "license": "MIT", - "dependencies": { - "@courselit/common-models": "^0.38.1", - "@courselit/icons": "^0.3.1", - "@courselit/state-management": "^0.38.1", - "@courselit/text-editor": "^0.15.1", - "@courselit/utils": "^0.38.1", - "@radix-ui/react-avatar": "^1.0.3", - "@radix-ui/react-checkbox": "^1.0.4", - "@radix-ui/react-dialog": "^1.0.4", - "@radix-ui/react-dropdown-menu": "^2.0.5", - "@radix-ui/react-form": "^0.0.3", - "@radix-ui/react-switch": "^1.0.3", - "@radix-ui/react-tabs": "^1.0.4", - "@radix-ui/react-toast": "^1.1.4", - "@radix-ui/react-tooltip": "^1.0.6", - "currency-symbol-map": "^5.1.0", - "react-dom": "^18.2.0" - }, - "devDependencies": { - "@types/react": "^17.0.43", - "rimraf": "^4.1.1", - "tailwind-config": "^0.3.1", - "tsconfig": "^0.3.1", - "tsup": "6.6.0", - "typescript": "^5.1.6" - }, - "peerDependencies": { - "next": "^13.4.11", - "react": "^18.2.0" - } - }, - "../../node_modules/.pnpm/@radix-ui+react-icons@1.3.0_react@18.2.0/node_modules/@radix-ui/react-icons": { - "version": "1.3.0", - "license": "MIT", - "devDependencies": { - "@modulz/generate-icon-lib": "^0.2.1", - "tsdx": "0.14.0" - }, - "peerDependencies": { - "react": "^16.x || ^17.x || ^18.x" - } - }, - "../../node_modules/.pnpm/@types+nodemailer@6.4.13/node_modules/@types/nodemailer": { - "version": "6.4.13", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "../../node_modules/.pnpm/eslint@8.51.0/node_modules/eslint": { - "version": "8.51.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.51.0", - "@humanwhocodes/config-array": "^0.11.11", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "devDependencies": { - "@babel/core": "^7.4.3", - "@babel/preset-env": "^7.4.3", - "@wdio/browser-runner": "^8.14.6", - "@wdio/cli": "^8.14.6", - "@wdio/concise-reporter": "^8.14.0", - "@wdio/globals": "^8.14.6", - "@wdio/mocha-framework": "^8.14.0", - "babel-loader": "^8.0.5", - "c8": "^7.12.0", - "chai": "^4.0.1", - "cheerio": "^0.22.0", - "common-tags": "^1.8.0", - "core-js": "^3.1.3", - "ejs": "^3.0.2", - "eslint": "file:.", - "eslint-config-eslint": "file:packages/eslint-config-eslint", - "eslint-plugin-eslint-comments": "^3.2.0", - "eslint-plugin-eslint-plugin": "^5.1.0", - "eslint-plugin-internal-rules": "file:tools/internal-rules", - "eslint-plugin-jsdoc": "^46.2.5", - "eslint-plugin-n": "^16.0.0", - "eslint-plugin-unicorn": "^42.0.0", - "eslint-release": "^3.2.0", - "eslump": "^3.0.0", - "esprima": "^4.0.1", - "fast-glob": "^3.2.11", - "fs-teardown": "^0.1.3", - "glob": "^7.1.6", - "got": "^11.8.3", - "gray-matter": "^4.0.3", - "lint-staged": "^11.0.0", - "load-perf": "^0.2.0", - "markdownlint": "^0.25.1", - "markdownlint-cli": "^0.31.1", - "marked": "^4.0.8", - "memfs": "^3.0.1", - "metascraper": "^5.25.7", - "metascraper-description": "^5.25.7", - "metascraper-image": "^5.29.3", - "metascraper-logo": "^5.25.7", - "metascraper-logo-favicon": "^5.25.7", - "metascraper-title": "^5.25.7", - "mocha": "^8.3.2", - "mocha-junit-reporter": "^2.0.0", - "node-polyfill-webpack-plugin": "^1.0.3", - "npm-license": "^0.3.3", - "pirates": "^4.0.5", - "progress": "^2.0.3", - "proxyquire": "^2.0.1", - "recast": "^0.20.4", - "regenerator-runtime": "^0.13.2", - "rollup-plugin-node-polyfills": "^0.2.1", - "semver": "^7.5.3", - "shelljs": "^0.8.2", - "sinon": "^11.0.0", - "vite-plugin-commonjs": "^0.8.2", - "webdriverio": "^8.14.6", - "webpack": "^5.23.0", - "webpack-cli": "^4.5.0", - "yorkie": "^2.0.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "../../node_modules/.pnpm/mongodb@6.2.0/node_modules/mongodb": { - "version": "6.2.0", - "license": "Apache-2.0", - "dependencies": { - "@mongodb-js/saslprep": "^1.1.0", - "bson": "^6.2.0", - "mongodb-connection-string-url": "^2.6.0" - }, - "devDependencies": { - "@iarna/toml": "^2.2.5", - "@istanbuljs/nyc-config-typescript": "^1.0.2", - "@microsoft/api-extractor": "^7.36.4", - "@microsoft/tsdoc-config": "^0.16.2", - "@mongodb-js/zstd": "^1.1.0", - "@octokit/core": "^4.2.4", - "@types/chai": "^4.3.5", - "@types/chai-subset": "^1.3.3", - "@types/express": "^4.17.17", - "@types/kerberos": "^1.1.2", - "@types/mocha": "^10.0.1", - "@types/node": "^20.5.9", - "@types/saslprep": "^1.0.1", - "@types/semver": "^7.5.0", - "@types/sinon": "^10.0.16", - "@types/sinon-chai": "^3.2.9", - "@types/whatwg-url": "^11.0.0", - "@typescript-eslint/eslint-plugin": "^5.62.0", - "@typescript-eslint/parser": "^5.62.0", - "chai": "^4.3.7", - "chai-subset": "^1.6.0", - "chalk": "^4.1.2", - "eslint": "^8.48.0", - "eslint-config-prettier": "^8.10.0", - "eslint-plugin-import": "^2.28.1", - "eslint-plugin-prettier": "^4.2.1", - "eslint-plugin-simple-import-sort": "^10.0.0", - "eslint-plugin-tsdoc": "^0.2.17", - "eslint-plugin-unused-imports": "^2.0.0", - "express": "^4.18.2", - "gcp-metadata": "^5.2.0", - "js-yaml": "^4.1.0", - "mocha": "^10.2.0", - "mocha-sinon": "^2.1.2", - "mongodb-client-encryption": "^6.0.0", - "mongodb-legacy": "^6.0.0", - "nyc": "^15.1.0", - "prettier": "^2.8.8", - "semver": "^7.5.4", - "sinon": "^15.2.0", - "sinon-chai": "^3.7.0", - "snappy": "^7.2.2", - "socks": "^2.7.1", - "source-map-support": "^0.5.21", - "ts-node": "^10.9.1", - "tsd": "^0.28.1", - "typescript": "^5.0.4", - "typescript-cached-transpile": "^0.0.6", - "v8-heapsnapshot": "^1.3.1", - "yargs": "^17.7.2" - }, - "engines": { - "node": ">=16.20.1" - }, - "peerDependencies": { - "@aws-sdk/credential-providers": "^3.188.0", - "@mongodb-js/zstd": "^1.1.0", - "gcp-metadata": "^5.2.0", - "kerberos": "^2.0.1", - "mongodb-client-encryption": ">=6.0.0 <7", - "snappy": "^7.2.2", - "socks": "^2.7.1" - }, - "peerDependenciesMeta": { - "@aws-sdk/credential-providers": { - "optional": true - }, - "@mongodb-js/zstd": { - "optional": true - }, - "gcp-metadata": { - "optional": true - }, - "kerberos": { - "optional": true - }, - "mongodb-client-encryption": { - "optional": true - }, - "snappy": { - "optional": true - }, - "socks": { - "optional": true - } - } - }, - "../../node_modules/.pnpm/nodemailer@6.9.6/node_modules/nodemailer": { - "version": "6.9.6", - "license": "MIT-0", - "devDependencies": { - "@aws-sdk/client-ses": "3.427.0", - "aws-sdk": "2.1472.0", - "bunyan": "1.8.15", - "chai": "4.3.10", - "eslint-config-nodemailer": "1.2.0", - "eslint-config-prettier": "9.0.0", - "grunt": "1.6.1", - "grunt-cli": "1.4.3", - "grunt-eslint": "24.3.0", - "grunt-mocha-test": "0.13.3", - "libbase64": "1.2.1", - "libmime": "5.2.1", - "libqp": "2.0.1", - "mocha": "10.2.0", - "nodemailer-ntlm-auth": "1.0.4", - "proxy": "1.0.2", - "proxy-test-server": "1.0.0", - "sinon": "16.1.0", - "smtp-server": "3.13.0" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "../../node_modules/.pnpm/react-icons@4.11.0_react@18.2.0/node_modules/react-icons": { - "version": "4.11.0", - "license": "MIT", - "peerDependencies": { - "react": "*" - } - }, - "../../node_modules/.pnpm/typescript@5.2.2/node_modules/typescript": { - "version": "5.2.2", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "devDependencies": { - "@esfx/canceltoken": "^1.0.0", - "@octokit/rest": "^19.0.13", - "@types/chai": "^4.3.4", - "@types/fs-extra": "^9.0.13", - "@types/glob": "^8.1.0", - "@types/microsoft__typescript-etw": "^0.1.1", - "@types/minimist": "^1.2.2", - "@types/mocha": "^10.0.1", - "@types/ms": "^0.7.31", - "@types/node": "latest", - "@types/source-map-support": "^0.5.6", - "@types/which": "^2.0.1", - "@typescript-eslint/eslint-plugin": "^6.0.0", - "@typescript-eslint/parser": "^6.0.0", - "@typescript-eslint/utils": "^6.0.0", - "azure-devops-node-api": "^12.0.0", - "c8": "^7.14.0", - "chai": "^4.3.7", - "chalk": "^4.1.2", - "chokidar": "^3.5.3", - "del": "^6.1.1", - "diff": "^5.1.0", - "esbuild": "^0.18.1", - "eslint": "^8.22.0", - "eslint-formatter-autolinkable-stylish": "^1.2.0", - "eslint-plugin-local": "^1.0.0", - "eslint-plugin-no-null": "^1.0.2", - "eslint-plugin-simple-import-sort": "^10.0.0", - "fast-xml-parser": "^4.0.11", - "fs-extra": "^9.1.0", - "glob": "^8.1.0", - "hereby": "^1.6.4", - "jsonc-parser": "^3.2.0", - "minimist": "^1.2.8", - "mocha": "^10.2.0", - "mocha-fivemat-progress-reporter": "^0.1.0", - "ms": "^2.1.3", - "node-fetch": "^3.2.10", - "source-map-support": "^0.5.21", - "tslib": "^2.5.0", - "typescript": "^5.0.2", - "which": "^2.0.2" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@auth/core": { - "version": "0.37.2", - "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.37.2.tgz", - "integrity": "sha512-kUvzyvkcd6h1vpeMAojK2y7+PAV5H+0Cc9+ZlKYDFhDY31AlvsB+GW5vNO4qE3Y07KeQgvNO9U0QUx/fN62kBw==", - "license": "ISC", - "dependencies": { - "@panva/hkdf": "^1.2.1", - "@types/cookie": "0.6.0", - "cookie": "0.7.1", - "jose": "^5.9.3", - "oauth4webapi": "^3.0.0", - "preact": "10.11.3", - "preact-render-to-string": "5.2.3" - }, - "peerDependencies": { - "@simplewebauthn/browser": "^9.0.1", - "@simplewebauthn/server": "^9.0.2", - "nodemailer": "^6.8.0" - }, - "peerDependenciesMeta": { - "@simplewebauthn/browser": { - "optional": true - }, - "@simplewebauthn/server": { - "optional": true - }, - "nodemailer": { - "optional": true - } - } - }, - "node_modules/@babel/runtime": { - "version": "7.23.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.7.tgz", - "integrity": "sha512-w06OXVOFso7LcbzMiDGt+3X7Rh7Ho8MmgPoWU3rarH+8upf+wSU/grlGbWzQyr3DkdN6ZeuMFjpdwW0Q+HxobA==", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", - "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz", - "integrity": "sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@floating-ui/core": { - "version": "1.6.9", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", - "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==", - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.9" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz", - "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.6.0", - "@floating-ui/utils": "^0.2.9" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", - "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", - "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", - "license": "MIT" - }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.1.tgz", - "integrity": "sha512-pn44xgBtgpEbZsu+lWf2KNb6OAf70X68k+yk69Ic2Xz11zHR/w24/U49XT7AeRwJ0Px+mhALhU5LPci1Aymk7A==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.1.0" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.1.tgz", - "integrity": "sha512-VfuYgG2r8BpYiOUN+BfYeFo69nP/MIwAtSJ7/Zpxc5QF3KS22z8Pvg3FkrSFJBPNQ7mmcUcYQFBmEQp7eu1F8Q==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.1.0" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.1.0.tgz", - "integrity": "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.1.0.tgz", - "integrity": "sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.1.0.tgz", - "integrity": "sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.1.0.tgz", - "integrity": "sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.1.0.tgz", - "integrity": "sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==", - "cpu": [ - "ppc64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.1.0.tgz", - "integrity": "sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.1.0.tgz", - "integrity": "sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.1.0.tgz", - "integrity": "sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.1.0.tgz", - "integrity": "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.1.tgz", - "integrity": "sha512-anKiszvACti2sGy9CirTlNyk7BjjZPiML1jt2ZkTdcvpLU1YH6CXwRAZCA2UmRXnhiIftXQ7+Oh62Ji25W72jA==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.1.0" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.1.tgz", - "integrity": "sha512-kX2c+vbvaXC6vly1RDf/IWNXxrlxLNpBVWkdpRq5Ka7OOKj6nr66etKy2IENf6FtOgklkg9ZdGpEu9kwdlcwOQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.1.0" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.1.tgz", - "integrity": "sha512-7s0KX2tI9mZI2buRipKIw2X1ufdTeaRgwmRabt5bi9chYfhur+/C1OXg3TKg/eag1W+6CCWLVmSauV1owmRPxA==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.1.0" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.1.tgz", - "integrity": "sha512-wExv7SH9nmoBW3Wr2gvQopX1k8q2g5V5Iag8Zk6AVENsjwd+3adjwxtp3Dcu2QhOXr8W9NusBU6XcQUohBZ5MA==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.1.0" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.1.tgz", - "integrity": "sha512-DfvyxzHxw4WGdPiTF0SOHnm11Xv4aQexvqhRDAoD00MzHekAj9a/jADXeXYCDFH/DzYruwHbXU7uz+H+nWmSOQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.1.0" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.1.tgz", - "integrity": "sha512-pax/kTR407vNb9qaSIiWVnQplPcGU8LRIJpDT5o8PdAx5aAA7AS3X9PS8Isw1/WfqgQorPotjrZL3Pqh6C5EBg==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.1.0" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.1.tgz", - "integrity": "sha512-YDybQnYrLQfEpzGOQe7OKcyLUCML4YOXl428gOOzBgN6Gw0rv8dpsJ7PqTHxBnXnwXr8S1mYFSLSa727tpz0xg==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.4.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.1.tgz", - "integrity": "sha512-WKf/NAZITnonBf3U1LfdjoMgNO5JYRSlhovhRhMxXVdvWYveM4kM3L8m35onYIdh75cOMCo1BexgVQcCDzyoWw==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.1.tgz", - "integrity": "sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.22", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", - "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@next/env": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.1.tgz", - "integrity": "sha512-cwK27QdzrMblHSn9DZRV+DQscHXRuJv6MydlJRpFSqJWZrTYMLzKDeyueJNN9MGd8NNiUKzDQADAf+dMLXX7YQ==", - "license": "MIT" - }, - "node_modules/@next/eslint-plugin-next": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.2.4.tgz", - "integrity": "sha512-O8ScvKtnxkp8kL9TpJTTKnMqlkZnS+QxwoQnJwPGBxjBbzd6OVVPEJ5/pMNrktSyXQD/chEfzfFzYLM6JANOOQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-glob": "3.3.1" - } - }, - "node_modules/@next/eslint-plugin-next/node_modules/fast-glob": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", - "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/@next/swc-darwin-arm64": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.1.tgz", - "integrity": "sha512-hjDw4f4/nla+6wysBL07z52Gs55Gttp5Bsk5/8AncQLJoisvTBP0pRIBK/B16/KqQyH+uN4Ww8KkcAqJODYH3w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.1.tgz", - "integrity": "sha512-q+aw+cJ2ooVYdCEqZVk+T4Ni10jF6Fo5DfpEV51OupMaV5XL6pf3GCzrk6kSSZBsMKZtVC1Zm/xaNBFpA6bJ2g==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.1.tgz", - "integrity": "sha512-wBQ+jGUI3N0QZyWmmvRHjXjTWFy8o+zPFLSOyAyGFI94oJi+kK/LIZFJXeykvgXUk1NLDAEFDZw/NVINhdk9FQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.1.tgz", - "integrity": "sha512-IIxXEXRti/AulO9lWRHiCpUUR8AR/ZYLPALgiIg/9ENzMzLn3l0NSxVdva7R/VDcuSEBo0eGVCe3evSIHNz0Hg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.1.tgz", - "integrity": "sha512-bfI4AMhySJbyXQIKH5rmLJ5/BP7bPwuxauTvVEiJ/ADoddaA9fgyNNCcsbu9SlqfHDoZmfI6g2EjzLwbsVTr5A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.1.tgz", - "integrity": "sha512-FeAbR7FYMWR+Z+M5iSGytVryKHiAsc0x3Nc3J+FD5NVbD5Mqz7fTSy8CYliXinn7T26nDMbpExRUI/4ekTvoiA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.1.tgz", - "integrity": "sha512-yP7FueWjphQEPpJQ2oKmshk/ppOt+0/bB8JC8svPUZNy0Pi3KbPx2Llkzv1p8CoQa+D2wknINlJpHf3vtChVBw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.1.tgz", - "integrity": "sha512-3PMvF2zRJAifcRNni9uMk/gulWfWS+qVI/pagd+4yLF5bcXPZPPH2xlYRYOsUjmCJOXSTAC2PjRzbhsRzR2fDQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@panva/hkdf": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", - "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@radix-ui/colors": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/colors/-/colors-3.0.0.tgz", - "integrity": "sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg==" - }, - "node_modules/@radix-ui/primitive": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", - "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==", - "dependencies": { - "@babel/runtime": "^7.13.10" - } - }, - "node_modules/@radix-ui/react-avatar": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.0.4.tgz", - "integrity": "sha512-kVK2K7ZD3wwj3qhle0ElXhOjbezIgyl2hVvgwfIdexL3rN6zJmy5AqqIf+D31lxVppdzV8CjAfZ6PklkmInZLw==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-context": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", - "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-primitive": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", - "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-slot": "1.0.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", - "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot/node_modules/@radix-ui/react-compose-refs": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", - "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", - "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", - "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collection": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.4.tgz", - "integrity": "sha512-cv4vSf7HttqXilDnAnvINd53OTl1/bjUYVZrkFnA7nwmY9Ob2POUy0WY0sfqBAe1s5FyKsyceQlqiEGPYNTadg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.0", - "@radix-ui/react-slot": "1.2.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz", - "integrity": "sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.5", - "@radix-ui/react-focus-guards": "1.0.1", - "@radix-ui/react-focus-scope": "1.0.4", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-portal": "1.0.4", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2", - "@radix-ui/react-use-controllable-state": "1.0.1", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.5.5" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-compose-refs": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", - "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-context": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", - "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz", - "integrity": "sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-escape-keydown": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", - "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz", - "integrity": "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-callback-ref": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-guards": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz", - "integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-scope": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz", - "integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", - "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-id": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz", - "integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-layout-effect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-id/node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", - "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-portal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.4.tgz", - "integrity": "sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-presence": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz", - "integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-presence/node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", - "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", - "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-slot": "1.0.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", - "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz", - "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-callback-ref": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-controllable-state/node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", - "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog/node_modules/react-remove-scroll": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", - "integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==", - "license": "MIT", - "dependencies": { - "react-remove-scroll-bar": "^2.3.3", - "react-style-singleton": "^2.2.1", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.0", - "use-sidecar": "^1.1.2" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.6.tgz", - "integrity": "sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-menu": "2.0.6", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-controllable-state": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-compose-refs": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", - "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-context": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", - "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-id": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz", - "integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-layout-effect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-id/node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", - "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-menu": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.0.6.tgz", - "integrity": "sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-collection": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.5", - "@radix-ui/react-focus-guards": "1.0.1", - "@radix-ui/react-focus-scope": "1.0.4", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-popper": "1.1.3", - "@radix-ui/react-portal": "1.0.4", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-roving-focus": "1.0.4", - "@radix-ui/react-slot": "1.0.2", - "@radix-ui/react-use-callback-ref": "1.0.1", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.5.5" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-collection": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz", - "integrity": "sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-direction": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.1.tgz", - "integrity": "sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz", - "integrity": "sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-escape-keydown": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz", - "integrity": "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-callback-ref": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-focus-guards": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz", - "integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-focus-scope": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz", - "integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-popper": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.3.tgz", - "integrity": "sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1", - "@radix-ui/react-use-rect": "1.0.1", - "@radix-ui/react-use-size": "1.0.1", - "@radix-ui/rect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-arrow": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz", - "integrity": "sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", - "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-use-rect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz", - "integrity": "sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/rect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-use-size": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz", - "integrity": "sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-layout-effect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-portal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.4.tgz", - "integrity": "sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-presence": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz", - "integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-presence/node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", - "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-roving-focus": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz", - "integrity": "sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-collection": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-controllable-state": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", - "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", - "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-menu/node_modules/react-remove-scroll": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", - "integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==", - "license": "MIT", - "dependencies": { - "react-remove-scroll-bar": "^2.3.3", - "react-style-singleton": "^2.2.1", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.0", - "use-sidecar": "^1.1.2" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-primitive": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", - "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-slot": "1.0.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", - "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz", - "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-callback-ref": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-use-controllable-state/node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", - "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-icons": { - "resolved": "../../node_modules/.pnpm/@radix-ui+react-icons@1.3.0_react@18.2.0/node_modules/@radix-ui/react-icons", - "link": true - }, - "node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-label": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.0.2.tgz", - "integrity": "sha512-N5ehvlM7qoTLx7nWPodsPYPgMzA5WM8zZChQg8nyFJKnDO5WHdba1vv5/H6IO5LtJMfD2Q3wh1qHFGNtK0w3bQ==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", - "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-slot": "1.0.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", - "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot/node_modules/@radix-ui/react-compose-refs": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", - "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.0.7.tgz", - "integrity": "sha512-shtvVnlsxT6faMnK/a7n0wptwBD23xc1Z5mdrtKLwVEfsEMXodS0r5s0/g5P0hX//EKYZS2sxUjqfzlg52ZSnQ==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.5", - "@radix-ui/react-focus-guards": "1.0.1", - "@radix-ui/react-focus-scope": "1.0.4", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-popper": "1.1.3", - "@radix-ui/react-portal": "1.0.4", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2", - "@radix-ui/react-use-controllable-state": "1.0.1", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.5.5" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-compose-refs": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", - "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-context": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", - "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz", - "integrity": "sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-escape-keydown": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", - "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz", - "integrity": "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-callback-ref": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-focus-guards": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz", - "integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-focus-scope": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz", - "integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", - "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-id": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz", - "integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-layout-effect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-id/node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", - "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-popper": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.3.tgz", - "integrity": "sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1", - "@radix-ui/react-use-rect": "1.0.1", - "@radix-ui/react-use-size": "1.0.1", - "@radix-ui/rect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-arrow": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz", - "integrity": "sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", - "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", - "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-use-rect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz", - "integrity": "sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/rect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-use-size": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz", - "integrity": "sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-layout-effect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-portal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.4.tgz", - "integrity": "sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-presence": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz", - "integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-presence/node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", - "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-primitive": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", - "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-slot": "1.0.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", - "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz", - "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-callback-ref": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-controllable-state/node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", - "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/react-remove-scroll": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", - "integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==", - "license": "MIT", - "dependencies": { - "react-remove-scroll-bar": "^2.3.3", - "react-style-singleton": "^2.2.1", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.0", - "use-sidecar": "^1.1.2" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-presence": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.3.tgz", - "integrity": "sha512-IrVLIhskYhH3nLvtcBLQFZr61tBG7wx7O3kEmdzcYwRGAEBmBicGGL7ATzNgruYJ3xBTbuzEEq9OXJM3PAX3tA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.0.tgz", - "integrity": "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-progress": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.4.tgz", - "integrity": "sha512-8rl9w7lJdcVPor47Dhws9mUHRHLE+8JEgyJRdNWCpGPa6HIlr3eh+Yn9gyx1CnCLbw5naHsI2gaO9dBWO50vzw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.7.tgz", - "integrity": "sha512-C6oAg451/fQT3EGbWHbCQjYTtbyjNO1uzQgMzwyivcHT3GKNEmu1q3UuREhN+HzHAVtv3ivMVK08QlC+PkYw9Q==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-collection": "1.1.4", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.0", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/primitive": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", - "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", - "license": "MIT" - }, - "node_modules/@radix-ui/react-separator": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.0.3.tgz", - "integrity": "sha512-itYmTy/kokS21aiV5+Z56MZB54KrhPgn6eHDKkFeOLR34HMN2s8PaN47qZZAGnvupcjxHaFZnW4pQEh0BvvVuw==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", - "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-slot": "1.0.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", - "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot/node_modules/@radix-ui/react-compose-refs": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", - "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slot": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz", - "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-switch": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.0.3.tgz", - "integrity": "sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-controllable-state": "1.0.1", - "@radix-ui/react-use-previous": "1.0.1", - "@radix-ui/react-use-size": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-compose-refs": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", - "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-context": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", - "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-primitive": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", - "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-slot": "1.0.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", - "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz", - "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-callback-ref": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-use-controllable-state/node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", - "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-use-previous": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.0.1.tgz", - "integrity": "sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-use-size": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz", - "integrity": "sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-layout-effect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-use-size/node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", - "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tabs": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.8.tgz", - "integrity": "sha512-4iUaN9SYtG+/E+hJ7jRks/Nv90f+uAsRHbLYA6BcA9EsR6GNWgsvtS4iwU2SP0tOZfDGAyqIT0yz7ckgohEIFA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.3", - "@radix-ui/react-primitive": "2.1.0", - "@radix-ui/react-roving-focus": "1.1.7", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/primitive": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", - "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", - "license": "MIT" - }, - "node_modules/@radix-ui/react-toast": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.1.5.tgz", - "integrity": "sha512-fRLn227WHIBRSzuRzGJ8W+5YALxofH23y0MlPLddaIpLpCDqdE0NZlS2NRQDRiptfxDeeCjgFIpexB1/zkxDlw==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-collection": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.5", - "@radix-ui/react-portal": "1.0.4", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-controllable-state": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1", - "@radix-ui/react-visually-hidden": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-collection": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz", - "integrity": "sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", - "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-compose-refs": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", - "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-context": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", - "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz", - "integrity": "sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-escape-keydown": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz", - "integrity": "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-callback-ref": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-portal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.4.tgz", - "integrity": "sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-presence": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz", - "integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-primitive": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", - "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-slot": "1.0.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", - "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", - "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz", - "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-callback-ref": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", - "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-visually-hidden": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.3.tgz", - "integrity": "sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", - "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", - "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-effect-event": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", - "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/rect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.0.1.tgz", - "integrity": "sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - } - }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rushstack/eslint-patch": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.11.0.tgz", - "integrity": "sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@swc/counter": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", - "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "license": "Apache-2.0" - }, - "node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@types/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true - }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true - }, - "node_modules/@types/node": { - "version": "20.10.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.6.tgz", - "integrity": "sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw==", - "dev": true, - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@types/nodemailer": { - "resolved": "../../node_modules/.pnpm/@types+nodemailer@6.4.13/node_modules/@types/nodemailer", - "link": true - }, - "node_modules/@types/react": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.0.tgz", - "integrity": "sha512-UaicktuQI+9UKyA4njtDOGBD/67t8YEBt2xdfqu8+gP9hqPUPsiXlNPcpS2gVdjmis5GKPG3fCxbQLVgxsQZ8w==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.1.1", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.1.tgz", - "integrity": "sha512-jFf/woGTVTjUJsl2O7hcopJ1r0upqoq/vIOoCj0yLh3RIXxWcljlpuZ+vEBRXsymD1jhfeJrlyTy/S1UW+4y1w==", - "devOptional": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.0.0" - } - }, - "node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" - }, - "node_modules/aria-hidden": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.3.tgz", - "integrity": "sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "is-array-buffer": "^3.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.findlast": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", - "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.findlastindex": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", - "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-shim-unscopables": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", - "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", - "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.tosorted": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", - "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", - "es-errors": "^1.3.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/ast-types-flow": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", - "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/async-function": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/autoprefixer": { - "version": "10.4.17", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz", - "integrity": "sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "browserslist": "^4.22.2", - "caniuse-lite": "^1.0.30001578", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/axe-core": { - "version": "4.10.3", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz", - "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==", - "dev": true, - "license": "MPL-2.0", - "engines": { - "node": ">=4" - } - }, - "node_modules/axobject-query": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", - "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bson": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/bson/-/bson-6.2.0.tgz", - "integrity": "sha512-ID1cI+7bazPDyL9wYy9GaQ8gEEohWvcUl/Yf0dIdutJxnmInEEyCsb4awy/OiBfall7zBA179Pahi3vCdFze3Q==", - "engines": { - "node": ">=16.20.1" - } - }, - "node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.16.0" - } - }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001587", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001587.tgz", - "integrity": "sha512-HMFNotUmLXn71BQxg8cijvqxnIAofforZOwGsxyXJ0qugTdspUF4sPSJ2vhgprHCB996tIDzEq1ubumPDV8ULA==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ] - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/class-variance-authority": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.0.tgz", - "integrity": "sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A==", - "dependencies": { - "clsx": "2.0.0" - }, - "funding": { - "url": "https://joebell.co.uk" - } - }, - "node_modules/class-variance-authority/node_modules/clsx": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", - "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", - "engines": { - "node": ">=6" - } - }, - "node_modules/client-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", - "license": "MIT" - }, - "node_modules/clsx": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", - "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "license": "MIT", - "optional": true, - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "license": "MIT", - "optional": true, - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "devOptional": true - }, - "node_modules/damerau-levenshtein": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", - "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/data-view-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/inspect-js" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-node-es": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", - "license": "MIT" - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" - }, - "node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" - }, - "node_modules/electron-to-chromium": { - "version": "1.4.673", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.673.tgz", - "integrity": "sha512-zjqzx4N7xGdl5468G+vcgzDhaHkaYgVcf9MqgexcTqsl2UHSCmOj/Bi3HAprg4BZCpC7HyD8a6nZl6QAZf72gw==", - "dev": true - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" - }, - "node_modules/enhanced-resolve": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", - "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/es-abstract": { - "version": "1.23.9", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", - "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.2", - "arraybuffer.prototype.slice": "^1.0.4", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "data-view-buffer": "^1.0.2", - "data-view-byte-length": "^1.0.2", - "data-view-byte-offset": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-set-tostringtag": "^2.1.0", - "es-to-primitive": "^1.3.0", - "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.0", - "get-symbol-description": "^1.1.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "internal-slot": "^1.1.0", - "is-array-buffer": "^3.0.5", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.2", - "is-regex": "^1.2.1", - "is-shared-array-buffer": "^1.0.4", - "is-string": "^1.1.1", - "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.0", - "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.3", - "object-keys": "^1.1.1", - "object.assign": "^4.1.7", - "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.3", - "safe-array-concat": "^1.1.3", - "safe-push-apply": "^1.0.0", - "safe-regex-test": "^1.1.0", - "set-proto": "^1.0.0", - "string.prototype.trim": "^1.2.10", - "string.prototype.trimend": "^1.0.9", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.3", - "typed-array-byte-length": "^1.0.3", - "typed-array-byte-offset": "^1.0.4", - "typed-array-length": "^1.0.7", - "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.18" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-iterator-helpers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", - "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", - "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.0.3", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.6", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "iterator.prototype": "^1.1.4", - "safe-array-concat": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", - "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/eslint": { - "resolved": "../../node_modules/.pnpm/eslint@8.51.0/node_modules/eslint", - "link": true - }, - "node_modules/eslint-config-next": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.2.4.tgz", - "integrity": "sha512-v4gYjd4eYIme8qzaJItpR5MMBXJ0/YV07u7eb50kEnlEmX7yhOjdUdzz70v4fiINYRjLf8X8TbogF0k7wlz6sA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@next/eslint-plugin-next": "15.2.4", - "@rushstack/eslint-patch": "^1.10.3", - "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", - "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", - "eslint-import-resolver-node": "^0.3.6", - "eslint-import-resolver-typescript": "^3.5.2", - "eslint-plugin-import": "^2.31.0", - "eslint-plugin-jsx-a11y": "^6.10.0", - "eslint-plugin-react": "^7.37.0", - "eslint-plugin-react-hooks": "^5.0.0" - }, - "peerDependencies": { - "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", - "typescript": ">=3.3.1" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/eslint-config-next/node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.29.1.tgz", - "integrity": "sha512-ba0rr4Wfvg23vERs3eB+P3lfj2E+2g3lhWcCVukUuhtcdUx5lSIFZlGFEBHKr+3zizDa/TvZTptdNHVZWAkSBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.29.1", - "@typescript-eslint/type-utils": "8.29.1", - "@typescript-eslint/utils": "8.29.1", - "@typescript-eslint/visitor-keys": "8.29.1", - "graphemer": "^1.4.0", - "ignore": "^5.3.1", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/eslint-config-next/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.29.1.tgz", - "integrity": "sha512-DkDUSDwZVCYN71xA4wzySqqcZsHKic53A4BLqmrWFFpOpNSoxX233lwGu/2135ymTCR04PoKiEEEvN1gFYg4Tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "8.29.1", - "@typescript-eslint/utils": "8.29.1", - "debug": "^4.3.4", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/eslint-config-next/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.29.1.tgz", - "integrity": "sha512-QAkFEbytSaB8wnmB+DflhUPz6CLbFWE2SnSCrRMEa+KnXIzDYbpsn++1HGvnfAsUY44doDXmvRkO5shlM/3UfA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.29.1", - "@typescript-eslint/types": "8.29.1", - "@typescript-eslint/typescript-estree": "8.29.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/eslint-config-next/node_modules/@typescript-eslint/parser": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.29.1.tgz", - "integrity": "sha512-zczrHVEqEaTwh12gWBIJWj8nx+ayDcCJs06yoNMY0kwjMWDM6+kppljY+BxWI06d2Ja+h4+WdufDcwMnnMEWmg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.29.1", - "@typescript-eslint/types": "8.29.1", - "@typescript-eslint/typescript-estree": "8.29.1", - "@typescript-eslint/visitor-keys": "8.29.1", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/eslint-config-next/node_modules/@typescript-eslint/scope-manager": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.29.1.tgz", - "integrity": "sha512-2nggXGX5F3YrsGN08pw4XpMLO1Rgtnn4AzTegC2MDesv6q3QaTU5yU7IbS1tf1IwCR0Hv/1EFygLn9ms6LIpDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.29.1", - "@typescript-eslint/visitor-keys": "8.29.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/eslint-config-next/node_modules/@typescript-eslint/types": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.29.1.tgz", - "integrity": "sha512-VT7T1PuJF1hpYC3AGm2rCgJBjHL3nc+A/bhOp9sGMKfi5v0WufsX/sHCFBfNTx2F+zA6qBc/PD0/kLRLjdt8mQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/eslint-config-next/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.29.1.tgz", - "integrity": "sha512-l1enRoSaUkQxOQnbi0KPUtqeZkSiFlqrx9/3ns2rEDhGKfTa+88RmXqedC1zmVTOWrLc2e6DEJrTA51C9iLH5g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.29.1", - "@typescript-eslint/visitor-keys": "8.29.1", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/eslint-config-next/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.29.1.tgz", - "integrity": "sha512-RGLh5CRaUEf02viP5c1Vh1cMGffQscyHe7HPAzGpfmfflFg1wUz2rYxd+OZqwpeypYvZ8UxSxuIpF++fmOzEcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.29.1", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/eslint-config-next/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-config-next/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/eslint-config-next/node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", - "dev": true, - "dependencies": { - "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-import-resolver-typescript": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.1.tgz", - "integrity": "sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==", - "dev": true, - "dependencies": { - "debug": "^4.3.4", - "enhanced-resolve": "^5.12.0", - "eslint-module-utils": "^2.7.4", - "fast-glob": "^3.3.1", - "get-tsconfig": "^4.5.0", - "is-core-module": "^2.11.0", - "is-glob": "^4.0.3" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts" - }, - "peerDependencies": { - "eslint": "*", - "eslint-plugin-import": "*" - } - }, - "node_modules/eslint-module-utils": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", - "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7" - }, - "engines": { - "node": ">=4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import": { - "version": "2.31.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", - "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rtsao/scc": "^1.1.0", - "array-includes": "^3.1.8", - "array.prototype.findlastindex": "^1.2.5", - "array.prototype.flat": "^1.3.2", - "array.prototype.flatmap": "^1.3.2", - "debug": "^3.2.7", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.12.0", - "hasown": "^2.0.2", - "is-core-module": "^2.15.1", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.8", - "object.groupby": "^1.0.3", - "object.values": "^1.2.0", - "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.8", - "tsconfig-paths": "^3.15.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" - } - }, - "node_modules/eslint-plugin-import/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint-plugin-import/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-plugin-jsx-a11y": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", - "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "aria-query": "^5.3.2", - "array-includes": "^3.1.8", - "array.prototype.flatmap": "^1.3.2", - "ast-types-flow": "^0.0.8", - "axe-core": "^4.10.0", - "axobject-query": "^4.1.0", - "damerau-levenshtein": "^1.0.8", - "emoji-regex": "^9.2.2", - "hasown": "^2.0.2", - "jsx-ast-utils": "^3.3.5", - "language-tags": "^1.0.9", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.8", - "safe-regex-test": "^1.0.3", - "string.prototype.includes": "^2.0.1" - }, - "engines": { - "node": ">=4.0" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" - } - }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint-plugin-react": { - "version": "7.37.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", - "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.8", - "array.prototype.findlast": "^1.2.5", - "array.prototype.flatmap": "^1.3.3", - "array.prototype.tosorted": "^1.1.4", - "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.2.1", - "estraverse": "^5.3.0", - "hasown": "^2.0.2", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.9", - "object.fromentries": "^2.0.8", - "object.values": "^1.2.1", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.5", - "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.12", - "string.prototype.repeat": "^1.0.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" - } - }, - "node_modules/eslint-plugin-react/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-react/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-plugin-react/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fastq": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz", - "integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "dev": true, - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "functions-have-names": "^1.2.3", - "hasown": "^2.0.2", - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-nonce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-symbol-description": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-tsconfig": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.2.tgz", - "integrity": "sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==", - "dev": true, - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/has-bigints": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/internal-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "license": "MIT", - "optional": true - }, - "node_modules/is-async-function": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-function": "^1.0.0", - "call-bound": "^1.0.3", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-boolean-object": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-data-view": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-finalizationregistry": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-string": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-symbols": "^1.1.0", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" - }, - "node_modules/iterator.prototype": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", - "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "get-proto": "^1.0.0", - "has-symbols": "^1.1.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/jackspeak": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", - "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jiti": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", - "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/jose": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", - "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "dev": true, - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/jsx-ast-utils": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", - "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/kareem": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.5.1.tgz", - "integrity": "sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA==", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/language-subtag-registry": { - "version": "0.3.23", - "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", - "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/language-tags": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", - "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", - "dev": true, - "license": "MIT", - "dependencies": { - "language-subtag-registry": "^0.3.20" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", - "engines": { - "node": ">=10" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lucide-react": { - "version": "0.487.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.487.0.tgz", - "integrity": "sha512-aKqhOQ+YmFnwq8dWgGjOuLc8V1R9/c/yOd+zDY4+ohsR2Jo05lSGc3WsstYPIzcTpeosN7LoCkLReUUITvaIvw==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/mongodb": { - "resolved": "../../node_modules/.pnpm/mongodb@6.2.0/node_modules/mongodb", - "link": true - }, - "node_modules/mongoose": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.0.3.tgz", - "integrity": "sha512-LJRT0yP4TW14HT4r2RkxqyvoTylMSzWpl5QOeVHTnRggCLQSpkoBdgbUtORFq/mSL2o9cLCPJz+6uzFj25qbHw==", - "dependencies": { - "bson": "^6.2.0", - "kareem": "2.5.1", - "mongodb": "6.2.0", - "mpath": "0.9.0", - "mquery": "5.0.0", - "ms": "2.1.3", - "sift": "16.0.1" - }, - "engines": { - "node": ">=16.20.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mongoose" - } - }, - "node_modules/mongoose/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/mpath": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", - "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/mquery": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", - "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", - "dependencies": { - "debug": "4.x" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/next": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/next/-/next-15.3.1.tgz", - "integrity": "sha512-8+dDV0xNLOgHlyBxP1GwHGVaNXsmp+2NhZEYrXr24GWLHtt27YrBPbPuHvzlhi7kZNYjeJNR93IF5zfFu5UL0g==", - "license": "MIT", - "dependencies": { - "@next/env": "15.3.1", - "@swc/counter": "0.1.3", - "@swc/helpers": "0.5.15", - "busboy": "1.6.0", - "caniuse-lite": "^1.0.30001579", - "postcss": "8.4.31", - "styled-jsx": "5.1.6" - }, - "bin": { - "next": "dist/bin/next" - }, - "engines": { - "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" - }, - "optionalDependencies": { - "@next/swc-darwin-arm64": "15.3.1", - "@next/swc-darwin-x64": "15.3.1", - "@next/swc-linux-arm64-gnu": "15.3.1", - "@next/swc-linux-arm64-musl": "15.3.1", - "@next/swc-linux-x64-gnu": "15.3.1", - "@next/swc-linux-x64-musl": "15.3.1", - "@next/swc-win32-arm64-msvc": "15.3.1", - "@next/swc-win32-x64-msvc": "15.3.1", - "sharp": "^0.34.1" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.1.0", - "@playwright/test": "^1.41.2", - "babel-plugin-react-compiler": "*", - "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", - "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", - "sass": "^1.3.0" - }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - }, - "@playwright/test": { - "optional": true - }, - "babel-plugin-react-compiler": { - "optional": true - }, - "sass": { - "optional": true - } - } - }, - "node_modules/next-auth": { - "version": "5.0.0-beta.25", - "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.25.tgz", - "integrity": "sha512-2dJJw1sHQl2qxCrRk+KTQbeH+izFbGFPuJj5eGgBZFYyiYYtvlrBeUw1E/OJJxTRjuxbSYGnCTkUIRsIIW0bog==", - "license": "ISC", - "dependencies": { - "@auth/core": "0.37.2" - }, - "peerDependencies": { - "@simplewebauthn/browser": "^9.0.1", - "@simplewebauthn/server": "^9.0.2", - "next": "^14.0.0-0 || ^15.0.0-0", - "nodemailer": "^6.6.5", - "react": "^18.2.0 || ^19.0.0-0" - }, - "peerDependenciesMeta": { - "@simplewebauthn/browser": { - "optional": true - }, - "@simplewebauthn/server": { - "optional": true - }, - "nodemailer": { - "optional": true - } - } - }, - "node_modules/next-themes": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.2.1.tgz", - "integrity": "sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==", - "peerDependencies": { - "next": "*", - "react": "*", - "react-dom": "*" - } - }, - "node_modules/next/node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", - "dev": true - }, - "node_modules/nodemailer": { - "resolved": "../../node_modules/.pnpm/nodemailer@6.9.6/node_modules/nodemailer", - "link": true - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/oauth4webapi": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.5.0.tgz", - "integrity": "sha512-DF3mLWNuxPkxJkHmWxbSFz4aE5CjWOsm465VBfBdWzmzX4Mg3vF8icxK+iKqfdWrIumBJ2TaoNQWx+SQc2bsPQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", - "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.groupby": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", - "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.values": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", - "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/own-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.6", - "object-keys": "^1.1.1", - "safe-push-apply": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, - "node_modules/path-scurry": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", - "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", - "dependencies": { - "lru-cache": "^9.1.1 || ^10.0.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", - "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", - "engines": { - "node": "14 || >=16.14" - } - }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/postcss": { - "version": "8.4.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", - "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", - "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-load-config": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", - "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "lilconfig": "^3.0.0", - "yaml": "^2.3.4" - }, - "engines": { - "node": ">= 14" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/postcss-load-config/node_modules/lilconfig": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.0.tgz", - "integrity": "sha512-p3cz0JV5vw/XeouBU3Ldnp+ZkBjE+n8ydJ4mcwBrOiXXPqNlrzGBqWs9X4MWF7f+iKUBu794Y8Hh8yawiJbCjw==", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/postcss-nested": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", - "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", - "dependencies": { - "postcss-selector-parser": "^6.0.11" - }, - "engines": { - "node": ">=12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.0.15", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz", - "integrity": "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" - }, - "node_modules/preact": { - "version": "10.11.3", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz", - "integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/preact" - } - }, - "node_modules/preact-render-to-string": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.3.tgz", - "integrity": "sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==", - "license": "MIT", - "dependencies": { - "pretty-format": "^3.8.0" - }, - "peerDependencies": { - "preact": ">=10" - } - }, - "node_modules/pretty-format": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", - "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==", - "license": "MIT" - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/react": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", - "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", - "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.26.0" - }, - "peerDependencies": { - "react": "^19.1.0" - } - }, - "node_modules/react-icons": { - "resolved": "../../node_modules/.pnpm/react-icons@4.11.0_react@18.2.0/node_modules/react-icons", - "link": true - }, - "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/react-remove-scroll-bar": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", - "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", - "license": "MIT", - "dependencies": { - "react-style-singleton": "^2.2.2", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-style-singleton": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", - "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", - "license": "MIT", - "dependencies": { - "get-nonce": "^1.0.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.1", - "which-builtin-type": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-array-concat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "has-symbols": "^1.1.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-push-apply": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/scheduler": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", - "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", - "devOptional": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/sharp": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.1.tgz", - "integrity": "sha512-1j0w61+eVxu7DawFJtnfYcvSv6qPFvfTaqzTQ2BLknVhHTwGS8sc63ZBF4rzkWMBVKybo4S5OBtDdZahh2A1xg==", - "hasInstallScript": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.3", - "semver": "^7.7.1" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.1", - "@img/sharp-darwin-x64": "0.34.1", - "@img/sharp-libvips-darwin-arm64": "1.1.0", - "@img/sharp-libvips-darwin-x64": "1.1.0", - "@img/sharp-libvips-linux-arm": "1.1.0", - "@img/sharp-libvips-linux-arm64": "1.1.0", - "@img/sharp-libvips-linux-ppc64": "1.1.0", - "@img/sharp-libvips-linux-s390x": "1.1.0", - "@img/sharp-libvips-linux-x64": "1.1.0", - "@img/sharp-libvips-linuxmusl-arm64": "1.1.0", - "@img/sharp-libvips-linuxmusl-x64": "1.1.0", - "@img/sharp-linux-arm": "0.34.1", - "@img/sharp-linux-arm64": "0.34.1", - "@img/sharp-linux-s390x": "0.34.1", - "@img/sharp-linux-x64": "0.34.1", - "@img/sharp-linuxmusl-arm64": "0.34.1", - "@img/sharp-linuxmusl-x64": "0.34.1", - "@img/sharp-wasm32": "0.34.1", - "@img/sharp-win32-ia32": "0.34.1", - "@img/sharp-win32-x64": "0.34.1" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/sift": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/sift/-/sift-16.0.1.tgz", - "integrity": "sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ==" - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "license": "MIT", - "optional": true, - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/sonner": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.3.1.tgz", - "integrity": "sha512-+rOAO56b2eI3q5BtgljERSn2umRk63KFIvgb2ohbZ5X+Eb5u+a/7/0ZgswYqgBMg8dyl7n6OXd9KasA8QF9ToA==", - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string.prototype.includes": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", - "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/string.prototype.matchall": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", - "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "regexp.prototype.flags": "^1.5.3", - "set-function-name": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.repeat": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", - "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-data-property": "^1.1.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-object-atoms": "^1.0.0", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/styled-jsx": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", - "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", - "license": "MIT", - "dependencies": { - "client-only": "0.0.1" - }, - "engines": { - "node": ">= 12.0.0" - }, - "peerDependencies": { - "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/sucrase": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", - "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "glob": "^10.3.10", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/sucrase/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tailwind-merge": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.2.0.tgz", - "integrity": "sha512-SqqhhaL0T06SW59+JVNfAqKdqLs0497esifRrZ7jOaefP3o64fdFNDMrAQWZFMxTLJPiHVjRLUywT8uFz1xNWQ==", - "dependencies": { - "@babel/runtime": "^7.23.5" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/dcastil" - } - }, - "node_modules/tailwindcss": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz", - "integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.5.3", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.0", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.19.1", - "lilconfig": "^2.1.0", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.23", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.1", - "postcss-nested": "^6.0.1", - "postcss-selector-parser": "^6.0.11", - "resolve": "^1.22.2", - "sucrase": "^3.32.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tailwindcss-animate": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", - "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", - "peerDependencies": { - "tailwindcss": ">=3.0.0 || insiders" - } - }, - "node_modules/tailwindcss/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" - }, - "node_modules/tsconfig-paths": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", - "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", - "dev": true, - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.15", - "reflect.getprototypeof": "^1.0.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typescript": { - "resolved": "../../node_modules/.pnpm/typescript@5.2.2/node_modules/typescript", - "link": true - }, - "node_modules/unbox-primitive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-bigints": "^1.0.2", - "has-symbols": "^1.1.0", - "which-boxed-primitive": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true - }, - "node_modules/update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/use-callback-ref": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", - "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sidecar": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", - "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", - "license": "MIT", - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-bigint": "^1.1.0", - "is-boolean-object": "^1.2.1", - "is-number-object": "^1.1.1", - "is-string": "^1.1.1", - "is-symbol": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "function.prototype.name": "^1.1.6", - "has-tostringtag": "^1.0.2", - "is-async-function": "^2.0.0", - "is-date-object": "^1.1.0", - "is-finalizationregistry": "^1.1.0", - "is-generator-function": "^1.0.10", - "is-regex": "^1.2.1", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.1.0", - "which-collection": "^1.0.2", - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yaml": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", - "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", - "engines": { - "node": ">= 14" - } - }, - "node_modules/zod": { - "version": "3.22.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", - "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - } - } -} diff --git a/apps/web/package.json b/apps/web/package.json index 1f50a692..4563de23 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -28,11 +28,9 @@ "clsx": "^2.1.0", "lucide-react": "^0.487.0", "medialit": "workspace:^", - "mongoose": "^8.0.1", + "mongoose": "^8.19.3", "next": "^15.5.7", - "next-auth": "5.0.0-beta.25", "next-themes": "^0.2.1", - "nodemailer": "^6.7.2", "react": "19.2.0", "react-dom": "19.2.0", "react-icons": "^4.11.0", @@ -44,7 +42,6 @@ "devDependencies": { "@types/json-schema": "^7.0.15", "@types/node": "20.10.6", - "@types/nodemailer": "^6.4.13", "@types/react": "19.2.2", "@types/react-dom": "19.2.2", "autoprefixer": "^10.4.17", diff --git a/docs/prds/mcp-server-prd.md b/docs/prds/mcp-server-prd.md new file mode 100644 index 00000000..5a5bf39b --- /dev/null +++ b/docs/prds/mcp-server-prd.md @@ -0,0 +1,1024 @@ +# MCP Server for MediaLit API + +**Issue:** #185 +**Status:** Implemented +**Author:** Rajat Saxena +**Date:** 2026-06-13 +**Last revised:** 2026-06-17 + +> ## ✅ OAuth Restart-Safety Revision (2026-06-14) — implemented +> +> The original OAuth implementation in `src/oauth/` used **in-memory `Map` +> storage for access and refresh tokens**, so **every server restart invalidated +> every issued access and refresh token** (DCR client registrations survived). +> Clients had to re-authorize after any restart, deployment, or crash — which +> contradicted the published `refreshTokenLifetime: 30 days`. +> +> **Resolution (now in code):** access and refresh tokens are **stateless +> HS256-signed JWTs** (`src/oauth/jwt.ts`), self-contained and verifiable without +> any state lookup. `src/oauth/model.ts` no longer keeps token `Map`s — only an +> in-memory auth-code `Map` (5-min TTL). Refresh-token revocation is persisted +> in MongoDB so explicit `/oauth/revoke` survives redeploys. See §6.7 for the +> full design. + +## 1. Objective + +Build a [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server, embedded in the MediaLit Express API, that exposes file management capabilities as MCP tools over **Streamable HTTP** transport. This allows AI coding agents (Claude Code, Cursor, etc.) and MCP-compatible clients to list, read, delete, and manage media files programmatically using natural language or tool calls. + +## 2. Architecture + +### 2.1 Transport: Streamable HTTP (only) + +The MCP server uses **Streamable HTTP** transport — a single HTTP POST endpoint mounted inside the Express app. No stdio, no separate process. + +**Why Streamable HTTP:** + +- No separate process to manage — one `pnpm dev` starts everything +- Reuses existing Express port, TLS certificate, and middleware stack +- MCP clients connect via URL: `http://localhost:8000/mcp` (dev) or `https://api.medialit.cloud/mcp` (production) +- Auth via both the existing `apikey` middleware (CLI/agent clients) and OAuth 2.0 (browser-based/ChatGPT clients) + +### 2.2 Integration with Express + +The MCP server supports two authentication paths. CLI and agent clients (Claude Code, Cursor) use the existing `x-medialit-apikey` header. Browser-based clients and OAuth-only clients (ChatGPT connectors) use the OAuth 2.0 Authorization Code + PKCE flow. + +The OAuth 2.0 Authorization Server is a **standalone generic module** at `src/oauth/` — not tied to MCP. It exposes endpoints at `/oauth/*` that any consumer (MCP, web frontend, mobile app) can use. There are **no** MCP-specific `/mcp/authorize` / `/mcp/token` aliases — MCP clients discover the `/oauth/*` endpoints through `/.well-known/oauth-authorization-server`, so no backward-compat alias was needed. + +``` + ┌──────────────────────────────────────────────────────────────┐ + │ Express API (port 8000) │ + │ │ + │ /media/* → REST routes (multipart + TUS upload) │ ◄── upload files + │ /settings/* → REST routes │ + │ │ + │ ── OAuth 2.0 Authorization Server (standalone) ──────── │ + │ /.well-known/oauth-authorization-server → metadata │ ◄── OAuth discovery + │ /oauth/authorize → authorization page (OTP login) │ ◄── user login + │ /oauth/token → token exchange / refresh │ ◄── get access token + │ /oauth/revoke → token revocation │ ◄── logout + │ /oauth/userinfo → current OAuth user profile │ ◄── web session user + │ /oauth/register → DCR (RFC 7591) │ ◄── client registration + │ │ + │ ── MCP transport ────────────────────────────────────── │ + │ /mcp → MCP Streamable HTTP transport │ ◄── tools here + │ (auth: Bearer token OR x-medialit-apikey) │ + └──────────┬───────────────────────────────────────────────────┘ + │ + │ Path A — API key clients (Claude Code, Cursor) + │ POST /mcp + x-medialit-apikey: + │ + │ Path B — OAuth clients (ChatGPT, web, mobile) + │ POST /mcp + Authorization: Bearer *** + ▼ + ┌──────────────────────────────────┐ + │ MCP Client │ + │ (Claude Code / Cursor / ChatGPT) │ + └──────────────────────────────────┘ +``` + +- The MCP transport is mounted as Express middleware at `/mcp` +- A unified auth middleware checks for `Authorization: Bearer ***` first, then falls back to `x-medialit-apikey` +- `apps/api/src/index.ts` mounts `mcp/routes.ts`; that router owns the OAuth discovery/routes and the MCP transport route +- The OAuth server is mounted at `/oauth/*` (and `/.well-known/oauth-authorization-server` for discovery); there are no `/mcp/*` OAuth aliases +- MCP tools call service-layer functions directly — no HTTP calls to self +- Always enabled, no feature flag + +### 2.3 Upload Flow + +File uploads are supported via two paths: + +**Path A — REST API (multipart):** The existing `POST /media/create` endpoint accepts binary files as multipart/form-data. Suitable for large files and direct client uploads. + +**Path B — MCP tool (base64):** The `upload_media` MCP tool accepts files encoded as base64 strings in a JSON-RPC call. The server decodes the base64 content, writes it to a temp file, and calls the same `mediaService.upload()` function used by the REST route. Suitable for AI agents uploading small-to-medium files inline. + +**Path C — TUS resumable upload:** The `POST /media/create/resumable` TUS endpoint accepts resumable uploads and finalizes them through the same media service path. + +``` + ┌──────────┐ multipart upload ┌──────────────┐ + │ Client │ ──────────────────────→ │ /media/create│ + │ (REST) │ │ (REST) │ + │ │ returns media └──────────────┘ + │ │ ←────────────────────── + │ │ + │ Client │ POST /mcp (JSON-RPC) + │ (MCP) │ { upload_media, base64... } + │ │ ──────────────────────────→ upload_media tool + │ │ ←────────────────────────── returns media + │ │ (record is now temp=true) + │ │ + │ Client │ POST /mcp (JSON-RPC) + │ │ { seal_media, mediaId } + │ │ ──────────────────────────→ seal_media tool + │ │ ←────────────────────────── (temp flag cleared, + │ │ now visible to list_media) + │ │ + │ Client │ POST /mcp (JSON-RPC) + │ │ { list_media, group, ... } + │ │ ──────────────────────────→ list_media tool + │ │ ←────────────────────────── returns { mediaItems } + └──────────┘ +``` + +**Rationale for base64 in MCP:** + +- MCP transport (Streamable HTTP) uses JSON-RPC — not designed for binary file transfer +- Base64 encoding allows file bytes to be embedded in the JSON payload +- REST multipart and MCP uploads share the same `mediaService.upload()` implementation, ensuring consistent processing (WebP conversion, thumbnails, S3 upload) +- REST multipart, TUS, and MCP uploads all call `validateUploadConstraints()` so account storage limits and per-file size limits are enforced consistently before storage work begins + +**Two-step upload (temp → sealed):** + +Every record created by `mediaService.upload()` is flagged `temp: true` in the database. Records with `temp: true` are **excluded** from `list_media`, `get_media_count`, `get_total_storage`, and `get_paginated_media` queries (the `getPaginatedMedia` / `getMediaCount` filters in `src/media/queries.ts` apply `temp: { $ne: true }`). To make a record permanently visible, the caller must invoke `seal_media` with the returned `mediaId`. The `seal` operation `$unset`s the `temp` field. + +This is the same two-step behavior as the existing REST API (`POST /media/create` → `POST /media/seal/{mediaId}`), preserved for consistency. The temp flag also enables a periodic cleanup sweep (`src/media/cleanup.ts`) that deletes orphaned temp records older than a configurable TTL. + +> **Callout:** If an agent calls `upload_media` and the new file does not appear in `list_media`, the most common cause is that `seal_media` was not called. Always pair `upload_media` with `seal_media` unless the upload is intentionally a draft. + +### 2.4 Why not stdio + +Remote HTTP is strictly better for this use case: + +- **Single deployment:** The MCP endpoint ships with the API — no separate binary, no extra infra +- **Auth reuse:** The same middleware stack protects both REST and MCP endpoints +- **Client flexibility:** Claude Code, Cursor, IDE plugins, ChatGPT connectors, and custom UIs all connect the same way +- **No process lifecycle management:** The MCP server lives and dies with the Express process + +## 3. File Structure + +``` +apps/api/src/ +├── mcp/ +│ ├── routes.ts ← Express router for OAuth discovery/routes, CORS, +│ │ rate limiting, MCP auth, and /mcp sessions +│ ├── server.ts ← Creates McpServer with StreamableHTTPTransport, +│ │ imports + registers all tools +│ └── tools/ +│ ├── media.ts ← list_media, get_media, get_media_count, +│ │ get_total_storage, delete_media, seal_media +│ ├── responses.ts ← Shared MCP error responses +│ ├── schemas.ts ← Shared output schemas for structuredContent +│ ├── signature.ts ← create_upload_signature +│ ├── settings.ts ← get_media_settings, update_media_settings +│ └── upload.ts ← upload_media +│ +├── oauth/ +│ ├── routes.ts ← Express Router for /.well-known and /oauth/* +│ ├── server.ts ← OAuth2Server instance +│ ├── model.ts ← AuthorizationCodeModel (in-memory auth codes, +│ │ JWT access/refresh tokens, DCR persistence) +│ ├── authorize-page.ts ← Templated authorization HTML page +│ ├── jwt.ts ← (NEW) HS256 sign/verify helpers +│ ├── pending-auth-model.ts +│ ├── client-model.ts +│ ├── revoked-token-model.ts +│ └── middleware.ts ← Express middleware for Bearer token +│ introspection on any route +│ +├── auth/ +│ ├── resolve-auth.ts ← Shared resolver: Bearer token OR +│ │ x-medialit-apikey +│ └── middleware.ts ← Shared REST/MCP auth middleware factory +│ +└── (rest of API structure) +``` + +The OAuth module is a **standalone generic OAuth 2.0 Authorization Server** — it has no dependency on MCP. Any consumer (MCP tools, web frontend API routes, mobile app backends) can import `oauth/middleware.ts` to validate Bearer tokens or call `oauth/model.ts` directly for token introspection. + +The shared auth middleware (`auth/middleware.ts`) imports from the oauth module to validate Bearer tokens and exposes REST and MCP adapters — it is a consumer of the generic OAuth server, not part of it. + +## 4. Dependencies + +Current `apps/api/package.json` dependencies: + +| Package | Version | Purpose | +| --------------------------- | -------- | ------------------------------------------ | +| `@modelcontextprotocol/sdk` | ^1.29.0 | MCP server, StreamableHTTPTransport, types | +| `@node-oauth/oauth2-server` | ^5.3.0 | OAuth 2.0 Authorization Code + PKCE server | +| `jsonwebtoken` | ^9.0.2 | HS256 access-token signing | +| `express-rate-limit` | ^7.5.0 | OAuth and MCP rate limiting | +| `zod` | ^3.25.76 | MCP and OAuth boundary validation | + +`@node-oauth/oauth2-server` handles the OAuth 2.0 protocol logic (authorization code flow, token issuance, PKCE verification, refresh token rotation). `jsonwebtoken` handles the **stateless verification of access tokens** issued by the model (see §6.7). + +## 5. Tool Specification + +> **Output contract:** Every tool that declares an `outputSchema` MUST also return a `structuredContent` field on success matching that schema. The server's output validation layer rejects any tool call whose success return omits `structuredContent` with `MCP error -32602: Output validation error`. The `content` text field is preserved for backward compatibility (clients may read the same data from either path) but is no longer the source of truth. + +### 5.1 Media + +| Tool Name | Calls | Input Parameters | Description | +| ------------------- | --------------------------- | ----------------------------------------------------------------------------------------------- | ----------------------------------------------- | +| `list_media` | `mediaService.getPage()` | `page?` (number, ≥1), `limit?` (number, ≥1), `access?` ("public"\|"private"), `group?` (string) | List media files with optional filters | +| `get_media` | `getMediaDetails()` handler | `mediaId` (string, required) | Get metadata for a specific media file | +| `get_media_count` | `getMediaCount()` handler | none | Get total number of media files | +| `get_total_storage` | `getTotalSpace()` handler | none | Get total storage used and max storage in bytes | +| `delete_media` | `deleteMedia()` handler | `mediaId` (string, required) | Permanently delete a media file | +| `seal_media` | `sealMedia()` handler | `mediaId` (string, required) | Mark a media file as finalized/processed | + +**Output:** + +- `list_media` returns `structuredContent: { mediaItems }`, where `mediaItems` is the current page of media list items. The text content is the JSON array for backward-compatible clients. +- `get_media` returns the full media object. `structuredContent` is the same media object and follows `mediaSchema`. +- `get_media_count` returns `{ count }`. +- `get_total_storage` returns `{ storage, maxStorage }`, both in bytes. +- `delete_media` returns `{ message: SUCCESS }`. +- `seal_media` returns the sealed media object. + +**Sealed-only filter (applies to the list/count/storage tools in this section):** The query helpers `getMediaCount`, `getTotalSpace`, and `getPaginatedMedia` in `src/media/queries.ts` filter out records that are still flagged `temp: true`. Newly uploaded records (via REST `POST /media/create`, TUS, or MCP `upload_media`) are created with `temp: true` and become visible to `list_media` / `get_media_count` / `get_total_storage` only **after** `seal_media` is invoked. See §2.3 for the full temp → seal flow. + +**Exception — `get_media`:** `getMedia` (the single-item lookup) does **not** apply the `temp: { $ne: true }` filter (the filter is intentionally commented out in `src/media/queries.ts`). This lets a caller fetch and inspect a freshly uploaded draft by its `mediaId` _before_ sealing it. + +### 5.2 Upload Signature + +| Tool Name | Calls | Input Parameters | Description | +| ------------------------- | ----------------------------- | ----------------- | -------------------------------------------------- | +| `create_upload_signature` | `generateSignature()` handler | `group?` (string) | Generate an HMAC signature for client-side uploads | + +**Output:** `{ signature }` — the HMAC signature value. `structuredContent: { signature }` is required on success. + +### 5.3 Settings + +| Tool Name | Calls | Input Parameters | Description | +| ----------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | +| `get_media_settings` | Media settings handler | none | Get current media processing configuration | +| `update_media_settings` | Media settings handler | `useWebP?` (boolean), `webpOutputQuality?` (number, 0–100), `thumbnailWidth?` (number), `thumbnailHeight?` (number) | Update media processing configuration | + +**Output:** + +- `get_media_settings` returns the full settings object: `{ useWebP, webpOutputQuality, thumbnailWidth, thumbnailHeight }`. +- `update_media_settings` returns `{ message: SUCCESS }`. + +### 5.4 Upload + +| Tool Name | Calls | Input Parameters | Auth | Annotations | Description | +| -------------- | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ----------------------------------------------- | ------------------------------------------------------------- | +| `upload_media` | `mediaService.upload()` | `fileBase64` (string, required — base64-encoded file content), `fileName` (string, required — filename with extension), `mimeType` (string, required — MIME type), `caption` (string, optional), `access` ("public"\|"private", optional), `group` (string, optional) | Required | `destructiveHint: false`, `openWorldHint: true` | Upload a file to MediaLit storage from base64-encoded content | + +The tool decodes the base64 string into a `Buffer`, writes it to a temp file in `os.tmpdir()`, constructs a file-like object with a `mv()` method, validates file size and account storage via `validateUploadConstraints()`, and calls `mediaService.upload()`. The temp file is removed in a `finally` block after the upload completes or fails. It then fetches the uploaded media details and returns the full media object in `structuredContent`. + +> **Two-step upload:** The record returned by `upload_media` is created with `temp: true` and is **not** visible to `list_media` / `get_media_count` / `get_total_storage` until the caller invokes `seal_media` with the returned `mediaId`. See §2.3 and §5.1. A typical agent workflow is: `upload_media` → `seal_media` (and then optionally `list_media` / `get_media` to confirm). + +## 6. OAuth 2.0 Authorization Server + +The authentication system has two independent mechanisms that can be used together or separately: + +- **OAuth 2.0 Authorization Code + PKCE** — A generic, standalone OAuth 2.0 Authorization Server module at `src/oauth/`. Used by ChatGPT MCP connectors, the web frontend (`apps/web`), and future mobile apps. +- **API Key (legacy)** — The existing `x-medialit-apikey` header auth, used by CLI/agent clients (Claude Code, Cursor). Handled by the shared auth resolver in `src/auth/resolve-auth.ts`. + +Both mechanisms resolve to the same internal `userId` before tools execute. + +### 6.1 Generic OAuth 2.0 Authorization Server (`src/oauth/`) + +The OAuth server is a **standalone module** with no dependency on MCP. It implements the Authorization Code flow with PKCE (RFC 7636) using `@node-oauth/oauth2-server`. No client secrets are needed — PKCE replaces them for public clients. + +User identity comes from the existing email-based OTP/magic link login (the User model). + +**Module structure:** + +``` +src/oauth/ +├── routes.ts ← Express Router +│ Endpoints: /oauth/authorize, /oauth/token, +│ /oauth/revoke, /oauth/userinfo, /oauth/register +├── server.ts ← OAuth2Server instance +├── model.ts ← AuthorizationCodeModel +│ (in-memory auth-codes; JWT access + refresh tokens) +├── jwt.ts ← HS256 sign/verify helpers, payload shape, key loading +├── authorize-page.ts ← Templated HTML authorization page +├── pending-auth-model.ts ← MongoDB-backed pending OTP/OAuth sessions +├── client-model.ts ← MongoDB-backed dynamic client registrations +├── revoked-token-model.ts ← MongoDB-backed revoked refresh-token JTIs +└── middleware.ts ← Express middleware: validate Bearer token + on any route (returns userId or null) +``` + +**Standardized endpoints:** + +| Endpoint | Method | Purpose | +| ----------------------------------------- | ------ | ------------------------------------------- | +| `/.well-known/oauth-authorization-server` | GET | OAuth discovery metadata | +| `/oauth/authorize` | GET | Authorization page (OTP login) | +| `/oauth/authorize/send-otp` | POST | Send OTP email | +| `/oauth/authorize/verify-otp` | POST | Verify OTP + issue auth code | +| `/oauth/token` | POST | Token exchange & refresh | +| `/oauth/userinfo` | GET | Current OAuth user's `{ sub, email, name }` | +| `/oauth/revoke` | POST | Token revocation | +| `/oauth/register` | POST | Dynamic client registration (RFC 7591) | + +The `src/oauth/` module is mounted by `src/mcp/routes.ts`, which is then mounted once from `src/index.ts` with `app.use(mcpRoutes)`. No MCP-specific `/mcp/authorize` / `/mcp/token` / `/mcp/revoke` aliases exist — they were considered for backward compat but never needed, since MCP clients discover the `/oauth/*` endpoints via `/.well-known/oauth-authorization-server`. + +`/oauth/register` is public for MCP client compatibility, but is rate-limited and accepts only public PKCE clients. Registered redirect URIs must be HTTPS, except localhost or loopback development URLs, and must not contain fragments or credentials. + +### 6.2 API Key Auth (legacy) + +Unchanged from the original design. Used by Claude Code, Cursor, and any programmatic client that can set custom HTTP headers. Defined in `src/auth/resolve-auth.ts` and exposed through the shared middleware factory in `src/auth/middleware.ts`. + +| Header | Value | Validated against | +| ------------------- | ----------- | --------------------------------------------------- | +| `x-medialit-apikey` | `` | Database (User model, existing `apikey` middleware) | + +Flow: + +1. Client sends `POST /mcp` with `x-medialit-apikey: ` +2. Auth middleware detects the header and delegates to existing `apikey` validation +3. On success: `req.user` is populated, tools execute +4. On failure: HTTP 401 + +### 6.3 Supported Client Types + +| Client | Type | Auth Method | Grant Type | Client Registration | +| ------------------------- | ------------ | ----------- | ------------------ | ----------------------------------------------- | +| ChatGPT / MCP | Confidential | PKCE + OTP | authorization_code | DCR (RFC 7591) or static pre-registration | +| Web frontend (`apps/web`) | Public | PKCE + OTP | authorization_code | First-party (pre-registered, e.g. `web-client`) | +| Mobile app (future) | Public | PKCE + OTP | authorization_code | DCR (RFC 7591) | +| CLI / Script (future) | Public | Device Code | device_code | Dynamic | + +**First-party clients** (web frontend, mobile app) are pre-registered in the OAuth model with known client IDs and redirect URIs. They use the same OAuth flow as third-party clients — no special bypass. The web frontend uses a custom PKCE flow with HTTP-only session cookies instead of NextAuth. + +### 6.4 Authorization Flow (OTP) + +The authorization endpoint is split into two stages. Our code owns the user-identity stage (OTP login); the library owns the code-generation stage. + +**Stage 1 — User login (our code):** + +1. Client redirects to `GET /oauth/authorize?response_type=code&client_id=...&redirect_uri=...&code_challenge=...&code_challenge_method=S256&state=...` +2. Server validates `client_id` and exact `redirect_uri` against registered clients +3. Server stashes the full query string (including PKCE parameters) in a short-lived session (TTL: 10 minutes) +4. Server renders an HTML login page — user enters their email address +5. Server sends an OTP/magic link email +6. User enters OTP; server validates it and resolves a `userId` + +**Stage 2 — Code generation (library):** + +7. After successful OTP verification, server calls the library's `authorize()` middleware passing the resolved `user` object +8. The library calls `saveAuthorizationCode()` on our model, which stores the code with `{ userId, redirectUri, codeChallenge, codeChallengeMethod, expiresAt }` +9. The library redirects to `redirect_uri?code=&state=` + +**Error responses** redirect to `redirect_uri?error=&error_description=&state=`. + +### 6.5 Usage by Consumers + +#### 6.5.1 MCP Client (ChatGPT / Claude Code) + +1. Client registers via DCR (`POST /oauth/register`) or uses a static client ID +2. Client initiates OAuth flow via the authorization page, user authenticates with OTP +3. Client receives authorization code and exchanges it at `POST /oauth/token` for an access token +4. Client sends the access token as `Authorization: Bearer ***` on every MCP request (`POST /mcp`) +5. The MCP auth middleware exported from `src/auth/middleware.ts` validates the token through `src/auth/resolve-auth.ts`, which calls `oauth/middleware.ts`'s `validateBearerToken()` + +#### 6.5.2 Web Frontend (`apps/web`) + +The Next.js frontend replaces NextAuth with a small first-party OAuth client: + +1. `apps/web/auth.ts` exposes `auth()` and `signOut()` helpers backed by HTTP-only cookies. `auth()` reads `session_user` and `session_access_token`; `signOut()` revokes `session_refresh_token`, clears local session cookies, and redirects to `/login`. +2. `apps/web/middleware.ts` protects application routes. It allows `/login`, `/api/auth/callback/medialit`, `/api/auth/signout`, static assets, and cleanup APIs, and redirects unauthenticated users to `/login`. +3. If the access token is missing, expired, or near expiry while a refresh token exists, middleware calls `POST /oauth/token` with `grant_type=refresh_token` and `client_id=web-client`, then stores the rotated `session_access_token` and `session_refresh_token` cookies. +4. `apps/web/app/login/route.ts` starts the PKCE flow by generating `state`, `code_verifier`, and `code_challenge`, storing the verifier in a short-lived HTTP-only `oauth_code_verifier` cookie, and redirecting to `/oauth/authorize`. +5. `apps/web/app/api/auth/callback/medialit/route.ts` exchanges the authorization code at `/oauth/token`, calls `/oauth/userinfo`, stores `session_access_token`, `session_refresh_token`, and `session_user`, clears `oauth_code_verifier`, and redirects to `/`. +6. `apps/web/app/api/auth/signout/route.ts` revokes the refresh token at `/oauth/revoke`, clears `session_access_token`, `session_refresh_token`, and `session_user`, and redirects to `/login`. + +#### 6.5.3 Mobile App (future) + +1. Registers as an OAuth client via DCR (`POST /oauth/register`) +2. Opens the system browser to `GET /oauth/authorize` with PKCE +3. After authorization, receives redirect with code and exchanges it for tokens at `POST /oauth/token` +4. Stores the refresh token securely and uses access tokens for API calls +5. Server-side API routes validate Bearer tokens via `oauth/middleware.ts` + +### 6.6 Auth Middleware Pattern + +The OAuth middleware at `src/oauth/middleware.ts` provides token introspection for any Express route: + +```typescript +import { verifyAccessToken } from "./jwt"; + +export async function validateBearerToken( + bearer: string, +): Promise<{ userId: string; clientId: string; scopes: string[] } | null> { + return verifyAccessToken(bearer); +} +``` + +This is used by `src/auth/resolve-auth.ts` for REST and MCP requests and can be used by any other consumer (web API routes, mobile app backends, etc.). + +### 6.7 OAuth Model & Server Configuration — **REVISED 2026-06-14** + +#### 6.7.1 Problem statement (was) + +The pre-revision `oauthModel` stored all tokens in **process-local `Map` instances**: + +```typescript +// src/oauth/model.ts (BEFORE) +const authorizationCodes = new Map(); // ✅ OK — 5 min TTL +const accessTokens = new Map(); // ❌ lost on restart +const refreshTokens = new Map(); // ❌ lost on restart +``` + +Tokens were **opaque random hex strings** (`crypto.randomBytes(32).toString("hex")`) with no internal structure — the server **had to look them up in its Map** to validate. Every server restart, crash, or redeploy wiped the Maps, immediately invalidating every access token and every refresh token in flight. This contradicts the published 30-day `refreshTokenLifetime`, so a client that successfully refreshed on day 1 would be silently broken on day 2 if the server restarted even once in between. + +#### 6.7.2 Resolution — stateless signed access tokens (HS256 JWT) + +Replace the in-memory access-token store with **HMAC-SHA-256 (HS256) JSON Web Tokens** that are self-contained and verifiable without any state lookup. The server signs them once at issuance and verifies the signature on every request. No Map. No restart invalidation. + +| Store | Pre-revision | Post-revision | +| ------------------ | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | +| **Auth codes** | In-memory `Map` (5 min TTL) | **In-memory `Map` (5 min TTL)** — unchanged. Codes are single-use and short-lived; restart loss is acceptable. | +| **Access tokens** | In-memory `Map` (opaque random hex) | **Signed JWT, HS256** (stateless, verified via `jsonwebtoken`) | +| **Refresh tokens** | In-memory `Map` (opaque random hex) | **Signed JWT, HS256, type=`refresh`** — verifiable on its own; persistent MongoDB `jti` deny-list for explicit revocation | + +**Why HS256 (symmetric) and not RS256/ES256 (asymmetric)?** MediaLit's OAuth is a single-tenant system — the same server that signs a token is the only server that verifies it. HS256 is simpler, faster, smaller, and has no key-distribution problem. Asymmetric keys only matter in multi-tenant/federated setups where a resource server needs to verify tokens without holding the signing key. + +#### 6.7.3 Signing-key management + +The signing key is loaded from environment variable `OAUTH_SIGNING_KEY`. The server refuses to start if the variable is missing or shorter than 32 bytes (256 bits — the minimum recommended HS256 key length per RFC 7518 §3.2). + +**Boot-time validation (in `src/index.ts` `checkConfig()`):** + +```typescript +if ( + !process.env.OAUTH_SIGNING_KEY || + Buffer.byteLength(process.env.OAUTH_SIGNING_KEY, "utf8") < 32 +) { + throw new Error( + "OAUTH_SIGNING_KEY is required and must be at least 32 bytes (256 bits). " + + "Generate one with: openssl rand -base64 48", + ); +} +``` + +**Key rotation policy:** A single `OAUTH_SIGNING_KEY` is used for both signing and verification. For rotation, deploy with a comma-separated list `OAUTH_SIGNING_KEY=,`. The first key signs new tokens; all listed keys are accepted for verification, so old tokens remain valid until they naturally expire. Once all old tokens have expired, drop the old key from the list. + +#### 6.7.4 Token payload shape + +```typescript +// Access token (HS256 JWT) +interface AccessTokenPayload { + sub: string; // userId + cid: string; // clientId + typ: "access"; // discriminator + scope: string; // OAuth space-delimited scope string; default: "" + iat: number; // issued-at (seconds) + exp: number; // expires-at (seconds) +} + +// Refresh token (HS256 JWT) +interface RefreshTokenPayload { + sub: string; // userId + cid: string; // clientId + typ: "refresh"; // discriminator + jti: string; // unique id, optional — used for revocation + iat: number; + exp: number; // 30 days from issuance +} +``` + +Claims are kept minimal — the JWT is **not** a session store, it's a capability assertion. There is no need to encode full user profile data, granted authorities beyond the `sub`, or anything else. If we later need server-side introspection (e.g. to invalidate a stolen token before its `exp`), the `jti` is the lookup key. + +#### 6.7.5 New module: `src/oauth/jwt.ts` + +```typescript +import jwt from "jsonwebtoken"; +import { randomUUID } from "crypto"; + +const KEYS = (process.env.OAUTH_SIGNING_KEY || "") + .split(",") + .map((s) => s.trim()) + .filter(Boolean); + +const SIGNING_KEY = KEYS[0]; // first key signs new tokens +const VERIFY_KEYS = KEYS; // all keys accepted for verification (rotation) + +const ACCESS_TOKEN_TTL = Number(process.env.TOKEN_TTL_SECONDS) || 900; +const REFRESH_TOKEN_TTL = 60 * 60 * 24 * 30; // 30 days + +export function signAccessToken( + userId: string, + clientId: string, + scope: string[] = [], +): string { + return jwt.sign( + { sub: userId, cid: clientId, typ: "access", scope: scope.join(" ") }, + SIGNING_KEY, + { algorithm: "HS256", expiresIn: ACCESS_TOKEN_TTL, noTimestamp: false }, + ); +} + +export function signRefreshToken(userId: string, clientId: string): string { + return jwt.sign( + { + sub: userId, + cid: clientId, + typ: "refresh", + jti: randomUUID(), + }, + SIGNING_KEY, + { algorithm: "HS256", expiresIn: REFRESH_TOKEN_TTL }, + ); +} + +/** + * Verify a token (access OR refresh). Returns the decoded payload on success, + * or null if the signature is invalid, the token is expired, or the type + * does not match `expectedType`. + */ +export function verifyToken( + token: string, + expectedType: "access" | "refresh", +): { sub: string; cid: string; scope: string[]; jti?: string } | null { + for (const key of VERIFY_KEYS) { + try { + const decoded = jwt.verify(token, key, { + algorithms: ["HS256"], + }) as any; + if (decoded.typ !== expectedType) return null; + return { + sub: decoded.sub, + cid: decoded.cid, + scope: normalizeScope(decoded.scope), + jti: decoded.jti, + }; + } catch { + // try next key + } + } + return null; +} + +export function verifyAccessToken(token: string) { + return verifyToken(token, "access"); +} + +export function verifyRefreshToken(token: string) { + return verifyToken(token, "refresh"); +} + +function normalizeScope(scope: unknown): string[] { + if (Array.isArray(scope)) { + return scope.filter((item): item is string => typeof item === "string"); + } + if (typeof scope === "string") { + return scope.split(/\s+/).filter(Boolean); + } + return []; +} +``` + +#### 6.7.6 Revised `oauthModel` + +The model's responsibilities shrink dramatically — it no longer needs to store or look up tokens. It only handles: + +- Client registration (DCR persistence) — **unchanged** +- Authorization code storage (short-lived, in-memory is fine) — **unchanged** +- PKCE verification — handled by the library, not the model +- Token **issuance** — now just signs JWTs and returns them to the library +- Token **revocation** — only refresh tokens are tracked in a persisted MongoDB deny-list (for explicit `/oauth/revoke` support); access tokens expire naturally + +```typescript +// src/oauth/model.ts (AFTER) +import { signAccessToken, signRefreshToken, verifyRefreshToken } from "./jwt"; +import OauthRevokedToken from "./revoked-token-model"; + +export const oauthModel: OAuth2Server.AuthorizationCodeModel = { + // ... getClient, saveAuthorizationCode, getAuthorizationCode, revokeAuthorizationCode + // ... unchanged from before ... + + async saveToken(token, client, user) { + const userId = String((user as any).id); + const clientId = (client as any).id; + + const accessToken = signAccessToken(userId, clientId); + const refreshToken = signRefreshToken(userId, clientId); + + return { + accessToken, + accessTokenExpiresAt: token.accessTokenExpiresAt, + refreshToken, + refreshTokenExpiresAt: token.refreshTokenExpiresAt, + scope: token.scope, + client: { id: clientId }, + user: { id: userId }, + custom: { userId }, + } as OAuth2Server.Token; + }, + + async getAccessToken(accessToken) { + const payload = verifyAccessToken(accessToken); + if (!payload) return null; + return { + accessToken, + accessTokenExpiresAt: new Date(payload.exp * 1000), + scope: payload.scope, + client: { id: payload.cid }, + user: { id: payload.sub }, + } as StoredAccessToken; + }, + + async getRefreshToken(refreshToken) { + const payload = verifyRefreshToken(refreshToken); + if (!payload) return null; + if ( + payload.jti && + (await OauthRevokedToken.findOne({ jti: payload.jti }).lean()) + ) { + return null; + } + return { + refreshToken, + refreshTokenExpiresAt: new Date(payload.exp * 1000), + scope: payload.scope, + client: { id: payload.cid }, + user: { id: payload.sub }, + } as StoredRefreshToken; + }, + + async revokeToken(token: StoredRefreshToken) { + const payload = verifyRefreshToken(token.refreshToken); + if (payload?.jti) { + await OauthRevokedToken.updateOne( + { jti: payload.jti }, + { + $setOnInsert: { + jti: payload.jti, + tokenType: "refresh_token", + userId: payload.sub, + clientId: payload.cid, + expiresAt: token.refreshTokenExpiresAt, + revokedAt: new Date(), + }, + }, + { upsert: true }, + ); + } + return true; + }, +}; +``` + +**Key behavior changes:** + +| Scenario | Pre-revision | Post-revision | +| ----------------------------------------- | ----------------------------------------------------------- | -------------------------------------------------------------- | +| Server restart, valid access token | ❌ Rejected (`invalid_token`) | ✅ Still valid (signature + `exp` only) | +| Server restart, valid refresh token | ❌ Rejected (`invalid_grant`) | ✅ Still valid (signature + `exp` + not in deny-list) | +| Server restart, valid auth code | ❌ Rejected (but it was already expired after 5 min anyway) | ❌ Rejected (same) — acceptable | +| Token `exp` reached | ❌ Rejected (sweep in 5 min) | ✅ Rejected immediately (verified at use) | +| Explicit `/oauth/revoke` of refresh token | ✅ Denied | ✅ Denied via persisted deny-list (`jti`) | +| Explicit `/oauth/revoke` of access token | ⚠️ Best-effort only | ⚠️ Still best-effort (JWT is stateless) — documented in §6.7.7 | +| Two API instances behind a load balancer | ❌ Tokens don't transfer between instances | ✅ Any instance can verify any token (stateless) | +| Compromised signing key | ❌ Attacker can mint tokens indefinitely | ✅ Rotate `OAUTH_SIGNING_KEY`; old tokens expire naturally | +| Token theft before `exp` | ⚠️ Cannot revoke | ⚠️ Same — `jti` deny-list only for refresh tokens | + +#### 6.7.7 Access-token revocation: known limitation + +JWT access tokens **cannot be selectively revoked before their `exp`**. The `/oauth/revoke` endpoint can deny-list a refresh token (via its `jti`), but revoking an access token before it naturally expires is not possible without a per-token server-side lookup — which is the token-store problem this revision is avoiding for normal API requests. + +This is an accepted, industry-standard trade-off. The mitigations are: + +- Access tokens are short-lived (15 minutes by default). +- Logout (`/oauth/revoke`) always revokes the refresh token, so the next refresh fails — the client must re-authorize. +- Stolen access tokens are only useful for the remaining token lifetime. After that, the attacker needs the refresh token (which has been revoked). +- Clients can be told to drop their cached access token on logout, which closes the window further. + +If a need arises to revoke access tokens in real time (e.g. compliance "right to be forgotten"), the path is: add a `jti` to every access token and a Redis-backed deny-list to the `verifyAccessToken` helper. This is a future enhancement, not in scope for this revision. + +#### 6.7.8 Other model changes (incidental, recommended as part of this revision) + +While we're in the file, fix the following pre-existing issues: + +1. **Refresh tokens must be rotated on use.** Set `alwaysIssueNewRefreshToken: true` on the `OAuth2Server` (already done in current code — confirm). +2. **Pending OAuth/OTP sessions are persisted to MongoDB** — the `oauthpendingauths` collection stores the short-lived authorization-page state with a TTL index, so the flow works across API restarts and multiple API instances. +3. **Authorization code storage remains in-memory** — codes are 5-minute, single-use, and there's nothing of value to persist. +4. **The `TOKEN_TTL_SECONDS` env var is the source of truth for access-token lifetime.** No new env var is needed for the refresh-token lifetime (it's hard-coded to 30 days; if we need to make it configurable later, that's a separate change). + +#### 6.7.9 Migration Plan (additive) + +| Step | What | Impact | +| ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 1 | Add `OAUTH_SIGNING_KEY` to `.env` (generate with `openssl rand -base64 48`) | New env var; no functional change yet | +| 2 | Add boot-time validation in `src/index.ts` `checkConfig()` | Server refuses to start without the key — fail-fast | +| 3 | Create `src/oauth/jwt.ts` with `signAccessToken` / `signRefreshToken` / `verifyToken` helpers | New file, no behavior change | +| 4 | Refactor `src/oauth/model.ts`: remove `accessTokens` and `refreshTokens` Maps; rewire `saveToken` / `getAccessToken` / `getRefreshToken` / `revokeToken` to use the JWT helpers; persist revoked refresh-token JTIs in MongoDB with a TTL index | **All existing tokens are invalidated** at the moment of deploy — clients must re-authorize once. This is the same one-time pain as the current restart behavior, except it happens exactly once and never again. | +| 5 | Add unit tests for `jwt.ts` (sign/verify/exp/type-mismatch/wrong-key/deny-list) | Required for any change that touches crypto | +| 6 | Verify the existing `validateBearerToken` contract in `oauth/middleware.ts` still returns the same shape it used to (userId). Document the new return type `{ userId, clientId, scopes }`. | The shared auth middleware should pick up `clientId` and `scopes` opportunistically for MCP requests (no breaking change — REST callers ignore them) | +| 7 | Update `.env.example` with the new variable and a doc comment on how to generate it | Docs only | + +**No DCR client data is affected** — dynamic-client registrations are persisted to the MongoDB `oauthclients` collection and are independent of the token store. + +#### 6.7.10 Configuration (post-revision) + +```typescript +// src/oauth/server.ts +const oauth = new OAuth2Server({ + model: oauthModel, + allowEmptyState: true, + allowExtendedTokenAttributes: true, + accessTokenLifetime: Number(process.env.TOKEN_TTL_SECONDS) || 900, + refreshTokenLifetime: 60 * 60 * 24 * 30, // 30 days + authorizationCodeLifetime: 5 * 60, + requireClientAuthentication: { + authorization_code: false, + refresh_token: false, + }, + alwaysIssueNewRefreshToken: true, // rotate refresh tokens on use +}); +``` + +| Store | Value | Lifetime | Survives restart? | +| ----------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------- | ----------------- | +| Auth code | `{ userId, redirectUri, codeChallenge, codeChallengeMethod }` | 5 minutes | ❌ (acceptable) | +| Pending OAuth/OTP state | `{ pendingId, clientId, redirectUri, otpHash, ... }` persisted to MongoDB `oauthpendingauths` | 10 minutes | ✅ | +| Access token | Signed JWT, stateless verification | 15 minutes (configurable via `TOKEN_TTL_SECONDS`) | ✅ | +| Refresh token | Signed JWT, persisted `jti` deny-list in MongoDB `oauthrevokedtokens` | 30 days | ✅ | +| DCR client registration | `{ clientId, redirectUris, grantTypes, ... }` persisted to MongoDB `oauthclients` | Indefinite | ✅ | + +### 6.8 Implemented Migration Shape + +| Step | What | Impact | +| ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | +| 1 | Create `src/oauth/` module — move `oauth-model.ts` → `oauth/model.ts`, split `oauth-server.ts` into `oauth/server.ts` + `oauth/authorize-page.ts` + `oauth/middleware.ts` | No functional change — imports still work | +| 2 | Split OAuth routing into `src/oauth/routes.ts` and keep the `OAuth2Server` instance in `src/oauth/server.ts` | Router code and server initialization have separate ownership | +| 3 | Mount `src/mcp/routes.ts` once from `src/index.ts`; `mcp/routes.ts` mounts OAuth discovery/routes and `/mcp` | `index.ts` stays thin; MCP-specific CORS/session handling lives in the MCP folder | +| 4 | Replace legacy web auth with the `apps/web` custom PKCE client that uses `/oauth/authorize`, `/oauth/token`, `/oauth/userinfo`, and `/oauth/revoke` | Web frontend uses the generic OAuth server without NextAuth | +| 5 | Add pre-registered first-party clients (`web-client`, `mobile-app`) to the model | New clients work out of the box | +| 6 | Do not expose legacy `/mcp/authorize`, `/mcp/token`, or `/mcp/revoke` aliases | OAuth clients discover the standard `/oauth/*` endpoints | +| 7 | **(2026-06-14, this revision)** Replace in-memory token store with stateless signed JWTs (see §6.7) | Solves restart invalidation bug; **all in-flight tokens invalidated once at deploy time** | + +## 7. Implementation Phases + +### Phase 1: Mount the Transport + Read-Only MVP + +**Goal:** MCP endpoint live at `/mcp` with read-only tools, testable via MCP Inspector using API key auth. + +Steps: + +1. Install `@modelcontextprotocol/sdk` +2. Create `mcp/server.ts` — `new McpServer({ name: "MediaLit", version: "1.0.0" })` with `StreamableHTTPServerTransport` +3. Register read-only tools: `list_media`, `get_media`, `get_media_count`, `get_total_storage`, `get_media_settings` (note: the `health_check` tool that shipped in the original rollout was removed in the Phase 3.5 cleanup — server reachability is now verified by the existing `/health` REST endpoint) +4. Create `auth/middleware.ts` with API key path only (OAuth path stubbed) +5. Mount in `apps/api/src/index.ts` through the MCP router: + + ```typescript + import mcpRoutes from "./mcp/routes"; + + app.use(mcpRoutes); + ``` + +6. Test with MCP Inspector: + ```bash + npx @modelcontextprotocol/inspector \ + --transport http \ + --url http://localhost:8000/mcp \ + -H "x-medialit-apikey: " + ``` + +**Deliverables:** All read tools functional via HTTP, API key auth working, Inspector passing. + +### Phase 2: Write Tools + +**Goal:** Full CRUD — delete, seal, signature, settings update, and direct upload. + +Steps: + +7. Implement `delete_media`, `seal_media`, `create_upload_signature`, `update_media_settings`, `upload_media` + +**Deliverables:** All 10 tools functional, end-to-end tested with real files. (Originally 11 tools, including `health_check`; that tool was removed in the Phase 3.5 cleanup — server reachability is verified via the existing `/health` REST endpoint.) + +### Phase 3: Generic OAuth 2.0 Authorization Server + +**Goal:** Standalone OAuth 2.0 Authorization Code + PKCE flow at `/oauth/*`, usable by MCP, web, and mobile. + +Steps: + +8. Install `@node-oauth/oauth2-server` +9. Create `src/oauth/` module: + - `model.ts` — `AuthorizationCodeModel` backed by in-memory authorization-code storage, stateless JWT access/refresh tokens, and DCR persistence in MongoDB `oauthclients` + - `server.ts` — create the `OAuth2Server` instance with `allowEmptyState: true`, `allowExtendedTokenAttributes: true`, public-client token exchange, and refresh-token rotation + - `routes.ts` — export the Express router for `/.well-known/oauth-authorization-server`, `GET /oauth/authorize`, `POST /oauth/token`, `GET /oauth/userinfo`, `POST /oauth/revoke`, `POST /oauth/register` + - `authorize-page.ts` — extract the inline HTML into a templated module + - `middleware.ts` — generic `validateBearerToken()` for any Express route +10. Build the Stage 1 authorization handler — email input → OTP send → OTP verify → call library's `authorize()` middleware with resolved user +11. Mount the OAuth router from `mcp/routes.ts` before the `/mcp` transport route, with no auth required for OAuth flow endpoints +12. Wire the `Authorization: Bearer` path into `auth/middleware.ts` using `auth/resolve-auth.ts` +13. Keep OAuth implementation out of `src/mcp/`; MCP consumes the generic OAuth module through shared auth middleware +14. Test the full flow using MCP Inspector OAuth mode +15. Connect ChatGPT MCP connector and verify end-to-end + +**Deliverables:** OAuth flow complete at `/oauth/*`, ChatGPT connector working, both auth paths tested, no MCP-specific OAuth aliases. + +### Phase 3.5: OAuth Restart-Safety Hardening (2026-06-14, **this revision**) + +**Goal:** Tokens survive server restart, deployment, crash, and horizontal scaling. Solves the bug where `hermes mcp login` works once, then the token becomes invalid on the next server restart, forcing the user to re-authorize. + +Steps: + +16. Generate a 48-byte signing key and add it to `.env` as `OAUTH_SIGNING_KEY` +17. Add boot-time validation in `src/index.ts` `checkConfig()` — refuse to start without the key (≥32 bytes) +18. Create `src/oauth/jwt.ts` with `signAccessToken`, `signRefreshToken`, `verifyToken`, `verifyAccessToken`, `verifyRefreshToken` (HS256) +19. Refactor `src/oauth/model.ts`: + - Remove `accessTokens` and `refreshTokens` `Map` instances + - `saveToken` — sign access+refresh JWTs, return them with correct `accessTokenExpiresAt` / `refreshTokenExpiresAt` + - `getAccessToken` — verify JWT, return synthetic `StoredAccessToken` shape + - `getRefreshToken` — verify JWT, check persisted deny-list, return synthetic `StoredRefreshToken` shape + - `revokeToken` — persist `jti` in MongoDB `oauthrevokedtokens` + - Keep `authorizationCodes` Map and `setInterval` sweep unchanged +20. Update `src/oauth/middleware.ts` to use the new `verifyAccessToken` helper, return `{ userId, clientId, scopes }` +21. Update `src/auth/middleware.ts` to consume the new richer return type (pick up `clientId` and `scopes` opportunistically for MCP mode — no breaking change) +22. Add unit tests in `apps/api/src/oauth/__tests__/jwt.test.ts` covering: sign/verify round-trip, expired token, wrong key, type mismatch, key rotation; add model tests for persisted refresh-token revocation +23. Update `.env.example` with `OAUTH_SIGNING_KEY=` and a doc comment +24. End-to-end smoke test: `hermes mcp login` → get a token → restart the server → confirm the same token still works on the next `tools/list` call + +**Deliverables:** Tokens survive server restart, deploys, crashes. One-time invalidation at deploy time is acceptable and documented. Unit tests in place for the JWT layer. + +### Phase 4: Polish & Testing + +**Goal:** Production quality — tests, validation, error handling. + +Steps: + +25. Add Zod input schema validation to all tools +26. Create `__tests__/mcp/` with unit tests for tool schemas and OAuth model methods +27. Audit error messages — `isError: true` with actionable text on all tool failures +28. Add `TOKEN_TTL_SECONDS` to `.env.example` +29. Ensure every tool with `outputSchema` returns matching `structuredContent` +30. Add file-size and account-storage constraint tests for REST multipart, TUS, and MCP upload paths + +**Deliverables:** Test suite, validation, production-ready error handling. + +### Phase 5: Web & Mobile Migration + +**Goal:** Migrate the Next.js frontend and enable mobile app support via the generic OAuth server. + +Steps: + +31. Add first-party client entries (`web-client`, `mobile-app`) to the OAuth model's static clients +32. Expose `/oauth/*` routes and discovery from `mcp/routes.ts` +33. Keep MCP-specific OAuth code out of `src/mcp/`; MCP requests resolve OAuth through `auth/resolve-auth.ts` +34. Replace the web frontend auth with the custom PKCE client: `auth.ts` cookie session helpers, `/login` PKCE redirect, `/api/auth/callback/medialit` token exchange + userinfo lookup, middleware refresh-token rotation, and `/api/auth/signout` revocation +35. Test the web frontend login flow end-to-end +36. Verify MCP Inspector, ChatGPT connector, and Claude Code all still work + +**Deliverables:** Generic OAuth server serving all clients, web frontend migrated, mobile-ready, no legacy MCP OAuth aliases. + +## 8. Error Handling + +All tools return errors as MCP content results with `isError: true`: + +```typescript +{ + content: [{ type: "text", text: "Failed to fetch media: mediaId not found" }], + isError: true, +} +``` + +**Error categories:** + +- **Auth errors (401):** Returned by `mcpAuth` middleware before tools run — MCP client sees HTTP 401 +- **OAuth errors (400):** Returned by the library per RFC 6749 (`error` + `error_description` JSON) +- **Not found:** "Media not found: {mediaId}" +- **Validation:** Zod schema validation failures with field-level detail +- **Service errors:** Errors from handlers/queries/S3 propagated with descriptive messages + +## 9. Testing + +| Test Type | Tool | Frequency | +| --------------------------- | ------------------------------------------------------------------------------------ | ------------------------ | +| **MCP Inspector (API key)** | Manual, `http://localhost:8000/mcp` + `x-medialit-apikey` header | During dev | +| **MCP Inspector (OAuth)** | Manual, OAuth mode → full authorize → token → tool calls | Phase 3 | +| **ChatGPT connector** | Add MediaLit MCP via ChatGPT settings, run tool calls in conversation | Phase 3 (manual) | +| **Unit tests** | `node:test` on tool schema validation, OAuth model methods, **JWT layer** | CI | +| **Integration tests** | Local API instance + Inspector tool call chain | CI (Phase 4) | +| **E2E with Claude Code** | `claude mcp add medialit --url http://localhost:8000/mcp` → natural language queries | Manual, before PR merge | +| **Restart-safety smoke** | Login → kill server → restart server → call `tools/list` with the cached token | Every deploy (Phase 3.5) | + +## 10. Integration Guide + +### Mount in Express + +In `apps/api/src/index.ts`, mount the MCP router after REST routes: + +```typescript +import mcpRoutes from "./mcp/routes"; + +app.use("/settings/media", mediaSettingsRoutes(passport)); +app.use("/media/signature", signatureRoutes); +app.use("/media", tusRoutes); +app.use("/media", mediaRoutes); +app.use(mcpRoutes); +``` + +`mcp/routes.ts` mounts the OAuth discovery/routes, applies MCP CORS and rate limiting, authenticates `/mcp` with the shared `mcpAuth` middleware, and manages Streamable HTTP sessions. + +The MCP transport and OAuth endpoints are always active — no feature flag. + +### Connect with Claude Code (API key) + +```bash +claude mcp add --transport http medialit https://api.medialit.cloud/mcp \ + --header "x-medialit-apikey: " +``` + +### Connect with Cursor (API key) + +In Cursor settings → MCP Servers → Add: + +``` +URL: https://api.medialit.cloud/mcp +Headers: { "x-medialit-apikey": } +``` + +### Connect with ChatGPT (OAuth) + +ChatGPT MCP connectors require OAuth 2.0 — API keys are not supported. Use the OAuth flow: + +1. In ChatGPT settings → Connectors → Add connector → Custom +2. Enter the MCP server URL: `https://api.medialit.cloud/mcp` +3. ChatGPT fetches `/.well-known/oauth-authorization-server` to discover endpoints +4. ChatGPT redirects you to `https://api.medialit.cloud/oauth/authorize` with a PKCE challenge +5. Enter your MediaLit email address — you receive an OTP/magic link +6. Complete login — you are redirected back to ChatGPT with an authorization code +7. ChatGPT exchanges the code at `POST /oauth/token` for an access token +8. All subsequent MCP requests use `Authorization: Bearer ***` +9. Access tokens expire after 15 minutes by default; ChatGPT automatically refreshes using the refresh token + +No API key is needed for this flow. + +### Connect with OAuth-Based MCP Clients (Programmatic) + +```typescript +// 1. Discover endpoints +const meta = await fetch( + "https://api.medialit.cloud/.well-known/oauth-authorization-server", +).then((r) => r.json()); + +// 2. Generate PKCE pair +const verifier = crypto.randomBytes(32).toString("base64url"); +const challenge = crypto + .createHash("sha256") + .update(verifier) + .digest("base64url"); + +// 3. Redirect user to authorization URL +const authUrl = new URL(meta.authorization_endpoint); +authUrl.searchParams.set("response_type", "code"); +authUrl.searchParams.set("client_id", "my-app"); +authUrl.searchParams.set("redirect_uri", "https://myapp.com/oauth/callback"); +authUrl.searchParams.set("code_challenge", challenge); +authUrl.searchParams.set("code_challenge_method", "S256"); +authUrl.searchParams.set("state", crypto.randomBytes(16).toString("hex")); +// → redirect user to authUrl.toString() + +// 4. After callback, exchange code for token +const tokens = await fetch(meta.token_endpoint, { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + grant_type: "authorization_code", + code: callbackCode, + redirect_uri: "https://myapp.com/oauth/callback", + client_id: "my-app", + code_verifier: verifier, + }), +}).then((r) => r.json()); + +// 5. Use access token with MCP client +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; + +const transport = new StreamableHTTPClientTransport({ + url: "https://api.medialit.cloud/mcp", + headers: { Authorization: `Bearer ${tokens.access_token}` }, +}); +const client = new Client({ name: "my-app", version: "1.0.0" }); +await client.connect(transport); +``` + +### Connect Programmatically with API Key + +```typescript +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; + +const transport = new StreamableHTTPClientTransport({ + url: "https://api.medialit.cloud/mcp", + headers: { "x-medialit-apikey": "" }, +}); +const client = new Client({ name: "my-app", version: "1.0.0" }); +await client.connect(transport); +``` + +## 11. Future Considerations + +- **Resource exposure:** Expose media files as MCP resources (`media://{mediaId}`) so Claude can reference them by URI +- **Prompts:** Add MCP prompt templates for common workflows ("upload via REST, then manage via MCP", "list recent uploads") +- **Pagination helper:** Convenience tool that fetches all pages of `list_media` for full inventory +- **Bulk operations:** Batch delete, batch seal +- **Rate limiting:** Per-key and per-token rate limiting for MCP endpoint +- **Logging:** Structured MCP request logging (separate from REST logs for observability) +- **Access-token revocation (real-time):** Add a `jti` to every access token and a Redis-backed deny-list for `verifyAccessToken`. Solves the "right to be forgotten" / stolen-token case. Deferred from this revision per §6.7.7. +- **Configured OAuth issuer:** Use an explicit `OAUTH_ISSUER` / `API_SERVER` value for authorization-server metadata instead of deriving issuer URLs from `Host` / `X-Forwarded-Host` headers. +- **JWT issuer/audience claims:** Add and validate `iss` and `aud` claims on OAuth access and refresh tokens. +- **Asymmetric keys (RS256/ES256):** Switch from HS256 to RS256/ES256 when we need a separate resource server (e.g. a CDN edge worker) to verify tokens without holding the signing key +- **Dynamic client registration (already done):** RFC 7591 endpoint so third-party apps can register without a code change +- **Scopes:** Fine-grained OAuth scopes (`mcp:read`, `mcp:write`) to allow read-only OAuth clients. Current OAuth access is effectively full account access. +- **Versioned transport:** Support multiple protocol versions if MCP spec evolves +- **Auto-seal on upload:** Add an optional `seal: true` parameter to `upload_media` that atomically uploads and seals in a single call. Useful for simple agents that don't need the draft/temp stage. The default (no `seal` arg) must remain the current two-step flow to preserve parity with the REST API. diff --git a/packages/models/package.json b/packages/models/package.json index 4cda9bf4..7ae9d0e1 100644 --- a/packages/models/package.json +++ b/packages/models/package.json @@ -17,6 +17,6 @@ }, "dependencies": { "@medialit/utils": "workspace:*", - "mongoose": "^8.0.1" + "mongoose": "^8.19.3" } } \ No newline at end of file diff --git a/packages/models/src/api-key-schema.ts b/packages/models/src/api-key-schema.ts index 680c14be..60c4da21 100644 --- a/packages/models/src/api-key-schema.ts +++ b/packages/models/src/api-key-schema.ts @@ -20,8 +20,8 @@ const ApikeySchema = new mongoose.Schema( }, httpReferrers: [String], ipAddresses: [String], - custom: String, deleted: { type: Boolean, default: false }, + default: { type: Boolean, default: false }, }, { timestamps: true, @@ -29,5 +29,12 @@ const ApikeySchema = new mongoose.Schema( ); ApikeySchema.index({ name: 1, userId: 1 }, { unique: true }); +ApikeySchema.index( + { userId: 1, default: 1 }, + { + unique: true, + partialFilterExpression: { default: true, deleted: false }, + }, +); export default ApikeySchema; diff --git a/packages/models/src/api-key.ts b/packages/models/src/api-key.ts index db109bb4..8093eb59 100644 --- a/packages/models/src/api-key.ts +++ b/packages/models/src/api-key.ts @@ -9,6 +9,6 @@ export interface Apikey { restriction?: APIKEY_RESTRICTION; httpReferrers?: string[]; ipAddresses?: string[]; - custom?: string; + default: boolean; deleted: boolean; } diff --git a/packages/models/src/index.ts b/packages/models/src/index.ts index 17e5797b..d384f195 100644 --- a/packages/models/src/index.ts +++ b/packages/models/src/index.ts @@ -1,8 +1,14 @@ export { Apikey } from "./api-key"; +export { OauthClient } from "./oauth-client"; +export { OauthPendingAuth } from "./oauth-pending-auth"; +export { OauthRevokedToken } from "./oauth-revoked-token"; export { User } from "./user"; export { APIKEY_RESTRICTION } from "./api-key-restriction"; export { default as UserSchema } from "./user-schema"; export { default as ApikeySchema } from "./api-key-schema"; +export { default as OauthClientSchema } from "./oauth-client-schema"; +export { default as OauthPendingAuthSchema } from "./oauth-pending-auth-schema"; +export { default as OauthRevokedTokenSchema } from "./oauth-revoked-token-schema"; export * as Constants from "./constants"; export { Media } from "./media"; export { default as MediaSchema } from "./media-schema"; diff --git a/packages/models/src/oauth-client-schema.ts b/packages/models/src/oauth-client-schema.ts new file mode 100644 index 00000000..a6e33a43 --- /dev/null +++ b/packages/models/src/oauth-client-schema.ts @@ -0,0 +1,27 @@ +import mongoose from "mongoose"; +import { OauthClient } from "./oauth-client"; + +const OauthClientSchema = new mongoose.Schema( + { + clientId: { type: String, required: true, unique: true }, + clientIdIssuedAt: { type: Number, required: true }, + redirectUris: { type: [String], required: true }, + grantTypes: { type: [String], required: true }, + tokenEndpointAuthMethod: { + type: String, + enum: ["none"], + required: true, + default: "none", + }, + clientName: String, + scope: String, + }, + { + timestamps: true, + }, +); + +OauthClientSchema.index({ clientId: 1 }, { unique: true }); +OauthClientSchema.index({ createdAt: 1 }); + +export default OauthClientSchema; diff --git a/packages/models/src/oauth-client.ts b/packages/models/src/oauth-client.ts new file mode 100644 index 00000000..de686f02 --- /dev/null +++ b/packages/models/src/oauth-client.ts @@ -0,0 +1,9 @@ +export interface OauthClient { + clientId: string; + clientIdIssuedAt: number; + redirectUris: string[]; + grantTypes: string[]; + tokenEndpointAuthMethod: "none"; + clientName?: string; + scope?: string; +} diff --git a/packages/models/src/oauth-pending-auth-schema.ts b/packages/models/src/oauth-pending-auth-schema.ts new file mode 100644 index 00000000..201d4de4 --- /dev/null +++ b/packages/models/src/oauth-pending-auth-schema.ts @@ -0,0 +1,28 @@ +import mongoose from "mongoose"; +import { OauthPendingAuth } from "./oauth-pending-auth"; + +const OauthPendingAuthSchema = new mongoose.Schema( + { + pendingId: { type: String, required: true, unique: true }, + clientId: { type: String, required: true }, + redirectUri: { type: String, required: true }, + codeChallenge: String, + codeChallengeMethod: String, + state: String, + scope: String, + email: String, + otpHash: String, + otpExpires: Date, + otpAttempts: { type: Number, default: 0 }, + otpSentAt: Date, + expiresAt: { type: Date, required: true }, + }, + { + timestamps: true, + }, +); + +OauthPendingAuthSchema.index({ pendingId: 1 }, { unique: true }); +OauthPendingAuthSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 }); + +export default OauthPendingAuthSchema; diff --git a/packages/models/src/oauth-pending-auth.ts b/packages/models/src/oauth-pending-auth.ts new file mode 100644 index 00000000..88c41f1e --- /dev/null +++ b/packages/models/src/oauth-pending-auth.ts @@ -0,0 +1,15 @@ +export interface OauthPendingAuth { + pendingId: string; + clientId: string; + redirectUri: string; + codeChallenge?: string; + codeChallengeMethod?: string; + state?: string; + scope?: string; + email?: string; + otpHash?: string; + otpExpires?: Date; + otpAttempts?: number; + otpSentAt?: Date; + expiresAt: Date; +} diff --git a/packages/models/src/oauth-revoked-token-schema.ts b/packages/models/src/oauth-revoked-token-schema.ts new file mode 100644 index 00000000..64aa8fa6 --- /dev/null +++ b/packages/models/src/oauth-revoked-token-schema.ts @@ -0,0 +1,26 @@ +import mongoose from "mongoose"; +import { OauthRevokedToken } from "./oauth-revoked-token"; + +const OauthRevokedTokenSchema = new mongoose.Schema( + { + jti: { type: String, required: true, unique: true }, + tokenType: { + type: String, + enum: ["refresh_token"], + required: true, + default: "refresh_token", + }, + userId: { type: String, required: true }, + clientId: { type: String, required: true }, + expiresAt: { type: Date, required: true }, + revokedAt: { type: Date, required: true, default: Date.now }, + }, + { + timestamps: true, + }, +); + +OauthRevokedTokenSchema.index({ jti: 1 }, { unique: true }); +OauthRevokedTokenSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 }); + +export default OauthRevokedTokenSchema; diff --git a/packages/models/src/oauth-revoked-token.ts b/packages/models/src/oauth-revoked-token.ts new file mode 100644 index 00000000..dd2c85ee --- /dev/null +++ b/packages/models/src/oauth-revoked-token.ts @@ -0,0 +1,8 @@ +export interface OauthRevokedToken { + jti: string; + tokenType: "refresh_token"; + userId: string; + clientId: string; + expiresAt: Date; + revokedAt: Date; +} diff --git a/packages/scripts/package.json b/packages/scripts/package.json index 0e1323d4..90347f56 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -9,7 +9,7 @@ "@aws-sdk/client-s3": "^3.0.0", "@medialit/models": "workspace:*", "dotenv": "^16.0.0", - "mongoose": "^8.0.0" + "mongoose": "^8.19.3" }, "devDependencies": { "ts-node": "^10.9.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aee6da05..0e3b0eb3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -71,6 +71,12 @@ importers: '@medialit/utils': specifier: workspace:^0.1.0 version: link:../../packages/utils + '@modelcontextprotocol/sdk': + specifier: ^1.29.0 + version: 1.29.0(zod@3.25.76) + '@node-oauth/oauth2-server': + specifier: ^5.3.0 + version: 5.3.0 '@tus/file-store': specifier: ^2.0.0 version: 2.0.0 @@ -92,15 +98,24 @@ importers: express-fileupload: specifier: ^1.5.2 version: 1.5.2 + express-rate-limit: + specifier: ^7.5.0 + version: 7.5.1(express@4.21.2) joi: specifier: ^17.6.0 version: 17.13.3 joi-to-swagger: specifier: ^6.2.0 version: 6.2.0(joi@17.13.3) + jsonwebtoken: + specifier: ^9.0.2 + version: 9.0.2 mongoose: specifier: ^8.19.3 version: 8.19.3 + nodemailer: + specifier: ^6.10.0 + version: 6.10.0 passport: specifier: ^0.7.0 version: 0.7.0 @@ -113,6 +128,9 @@ importers: swagger-ui-express: specifier: ^5.0.1 version: 5.0.1(express@4.21.2) + zod: + specifier: ^3.25.76 + version: 3.25.76 devDependencies: '@types/cors': specifier: ^2.8.12 @@ -126,12 +144,15 @@ importers: '@types/joi': specifier: ^17.2.3 version: 17.2.3 - '@types/mongoose': - specifier: ^5.11.97 - version: 5.11.97 + '@types/jsonwebtoken': + specifier: ^9.0.10 + version: 9.0.10 '@types/node': specifier: ^22.14.1 version: 22.14.1 + '@types/nodemailer': + specifier: ^6.4.17 + version: 6.4.17 '@types/passport': specifier: ^1.0.7 version: 1.0.17 @@ -173,7 +194,7 @@ importers: version: 14.2.10(@types/mdast@4.0.4)(@types/mdx@2.0.13)(@types/react@19.2.2)(fumadocs-core@16.7.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.2)(lucide-react@0.487.0(react@19.2.0))(next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(zod@4.3.6))(next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0) fumadocs-openapi: specifier: ^10.4.1 - version: 10.4.1(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(fumadocs-core@16.7.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.2)(lucide-react@0.487.0(react@19.2.0))(next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(zod@4.3.6))(fumadocs-ui@16.7.1(@types/mdx@2.0.13)(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(fumadocs-core@16.7.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.2)(lucide-react@0.487.0(react@19.2.0))(next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(zod@4.3.6))(next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(shiki@4.0.2)(tailwindcss@4.1.4))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(shiki@4.0.2) + version: 10.4.1(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(fumadocs-core@16.7.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.2)(lucide-react@0.487.0(react@19.2.0))(next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(zod@4.3.6))(fumadocs-ui@16.7.1(@types/mdx@2.0.13)(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(fumadocs-core@16.7.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.2)(lucide-react@0.487.0(react@19.2.0))(next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(zod@4.3.6))(next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(shiki@4.0.2)(tailwindcss@4.1.4))(json-schema-typed@8.0.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(shiki@4.0.2) fumadocs-ui: specifier: 16.7.1 version: 16.7.1(@types/mdx@2.0.13)(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(fumadocs-core@16.7.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.2)(lucide-react@0.487.0(react@19.2.0))(next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(zod@4.3.6))(next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(shiki@4.0.2)(tailwindcss@4.1.4) @@ -278,20 +299,14 @@ importers: specifier: workspace:^ version: link:../../packages/medialit mongoose: - specifier: ^8.0.1 - version: 8.13.2 + specifier: ^8.19.3 + version: 8.19.3 next: specifier: ^15.5.7 version: 15.5.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - next-auth: - specifier: 5.0.0-beta.25 - version: 5.0.0-beta.25(next@15.5.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(nodemailer@6.10.0)(react@19.2.0) next-themes: specifier: ^0.2.1 version: 0.2.1(next@15.5.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - nodemailer: - specifier: ^6.7.2 - version: 6.10.0 react: specifier: 19.2.0 version: 19.2.0 @@ -320,9 +335,6 @@ importers: '@types/node': specifier: 20.10.6 version: 20.10.6 - '@types/nodemailer': - specifier: ^6.4.13 - version: 6.4.17 '@types/react': specifier: 19.2.2 version: 19.2.2 @@ -446,8 +458,8 @@ importers: specifier: workspace:* version: link:../utils mongoose: - specifier: ^8.0.1 - version: 8.13.2 + specifier: ^8.19.3 + version: 8.19.3 devDependencies: typescript: specifier: ^5.2.2 @@ -465,7 +477,7 @@ importers: specifier: ^16.0.0 version: 16.6.1 mongoose: - specifier: ^8.0.0 + specifier: ^8.19.3 version: 8.19.3 devDependencies: '@types/node': @@ -513,20 +525,6 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} - '@auth/core@0.37.2': - resolution: {integrity: sha512-kUvzyvkcd6h1vpeMAojK2y7+PAV5H+0Cc9+ZlKYDFhDY31AlvsB+GW5vNO4qE3Y07KeQgvNO9U0QUx/fN62kBw==} - peerDependencies: - '@simplewebauthn/browser': ^9.0.1 - '@simplewebauthn/server': ^9.0.2 - nodemailer: ^6.8.0 - peerDependenciesMeta: - '@simplewebauthn/browser': - optional: true - '@simplewebauthn/server': - optional: true - nodemailer: - optional: true - '@aws-crypto/crc32@5.2.0': resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} engines: {node: '>=16.0.0'} @@ -1256,6 +1254,12 @@ packages: '@hapi/topo@5.1.0': resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} + '@hono/node-server@1.19.14': + resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -1499,8 +1503,15 @@ packages: '@mdx-js/mdx@3.1.1': resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==} - '@mongodb-js/saslprep@1.2.2': - resolution: {integrity: sha512-EB0O3SCSNRUFk66iRCpI+cXzIjdswfCs7F6nOC3RAGJ7xr5YhaicvsRwJ9eyzYvYRlCSDUO/c7g4yNulxKC1WA==} + '@modelcontextprotocol/sdk@1.29.0': + resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true '@mongodb-js/saslprep@1.3.2': resolution: {integrity: sha512-QgA5AySqB27cGTXBFmnpifAi7HxoGUeezwo6p9dI03MuDB6Pp33zgclqVb6oVK3j6I9Vesg0+oojW2XxB59SGg==} @@ -1616,6 +1627,13 @@ packages: cpu: [x64] os: [win32] + '@node-oauth/formats@1.0.0': + resolution: {integrity: sha512-DwSbLtdC8zC5B5gTJkFzJj5s9vr9SGzOgQvV9nH7tUVuMSScg0EswAczhjIapOmH3Y8AyP7C4Jv7b8+QJObWZA==} + + '@node-oauth/oauth2-server@5.3.0': + resolution: {integrity: sha512-gPnLZesfXWleX3Mwq9hch3yY9AiekT+cl+c99BoK7Yf/DQEoxXsvuVDG/D0MrPkFbfiOdR96Z88ZYZJkvQ5lAw==} + engines: {node: '>=16.0.0'} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1636,9 +1654,6 @@ packages: resolution: {integrity: sha512-a61ljmRVVyG5MC/698C8/FfFDw5a8LOIvyOLW5fztgUXqUpc1jOfQzOitSCbge657OgXXThmY3Tk8fpiDb4UcA==} engines: {node: '>= 20.0.0'} - '@panva/hkdf@1.2.1': - resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==} - '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} @@ -3027,9 +3042,6 @@ packages: '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} - '@types/cookie@0.6.0': - resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} - '@types/cors@2.8.17': resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} @@ -3076,8 +3088,8 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/jsonwebtoken@8.5.9': - resolution: {integrity: sha512-272FMnFGzAVMGtu9tkr29hRL6bZj4Zs1KZNeHLnKqAvp06tAIcarTMwOh8/8bz4FmKRcMxZhZNeUAQsNLoiPhg==} + '@types/jsonwebtoken@9.0.10': + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -3088,10 +3100,6 @@ packages: '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} - '@types/mongoose@5.11.97': - resolution: {integrity: sha512-cqwOVYT3qXyLiGw7ueU2kX9noE8DPGRY6z8eUxudhXY8NZ7DMKYAxyZkLSevGfhCX3dO/AoX5/SO9lAzfjon0Q==} - deprecated: Mongoose publishes its own types, so you do not need to install this package. - '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} @@ -3278,6 +3286,7 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@unrs/resolver-binding-darwin-arm64@1.4.1': resolution: {integrity: sha512-8Tv+Bsd0BjGwfEedIyor4inw8atppRxM5BdUnIt+3mAm/QXUm7Dw74CHnXpfZKXkp07EXJGiA8hStqCINAWhdw==} @@ -3362,6 +3371,10 @@ packages: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn-globals@4.3.4: resolution: {integrity: sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==} @@ -3398,6 +3411,14 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -3594,6 +3615,7 @@ packages: aws-sdk@2.1692.0: resolution: {integrity: sha512-x511uiJ/57FIsbgUe5csJ13k3uzu25uWQE+XqfBis/sB0SFoiElJWXRkgEAUh0U6n40eT3ay5Ue4oPkRMu1LYw==} engines: {node: '>= 10.0.0'} + deprecated: The AWS SDK for JavaScript (v2) has reached end-of-support, and no longer receives updates. Please migrate your code to use AWS SDK for JavaScript (v3). More info https://a.co/cUPnyil aws-sign2@0.7.0: resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==} @@ -3647,6 +3669,10 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + basic-auth@2.0.1: + resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} + engines: {node: '>= 0.8'} + bcrypt-pbkdf@1.0.2: resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} @@ -3665,6 +3691,10 @@ packages: resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + bowser@2.11.0: resolution: {integrity: sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==} @@ -3696,10 +3726,6 @@ packages: bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} - bson@6.10.3: - resolution: {integrity: sha512-MTxGsqgYTwfshYWTRdmZRC+M7FnG1b4y7RO7p2k3X24Wq0yv1m77Wsj0BzlPzd/IowgESfsruQCUToa7vbOpPQ==} - engines: {node: '>=16.20.1'} - bson@6.10.4: resolution: {integrity: sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==} engines: {node: '>=16.20.1'} @@ -3913,10 +3939,18 @@ packages: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + content-type@1.0.5: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} + content-type@2.0.0: + resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + engines: {node: '>=18'} + convert-source-map@1.9.0: resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} @@ -3926,6 +3960,10 @@ packages: cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + cookie@0.7.1: resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} engines: {node: '>= 0.6'} @@ -4016,6 +4054,15 @@ packages: supports-color: optional: true + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decamelize@1.2.0: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} @@ -4467,6 +4514,14 @@ packages: resolution: {integrity: sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==} engines: {node: '>=0.4.x'} + eventsource-parser@3.1.0: + resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + exec-sh@0.3.6: resolution: {integrity: sha512-nQn+hI3yp+oD0huYhKwvYI32+JFeq+XkNcD1GAo3Y/MjxsfVGmrrzrnzjWiNY6f+pUCP440fThsFh5gZrRAU/w==} @@ -4494,10 +4549,26 @@ packages: resolution: {integrity: sha512-wxUJn2vTHvj/kZCVmc5/bJO15C7aSMyHeuXYY3geKpeKibaAoQGcEv5+sM6nHS2T7VF+QHS4hTWPiY2mKofEdg==} engines: {node: '>=12.0.0'} + express-rate-limit@7.5.1: + resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express-rate-limit@8.5.2: + resolution: {integrity: sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + express@4.21.2: resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} engines: {node: '>= 0.10.0'} + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + extend-shallow@2.0.1: resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} engines: {node: '>=0.10.0'} @@ -4601,6 +4672,10 @@ packages: resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} engines: {node: '>= 0.8'} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + find-up@3.0.0: resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} engines: {node: '>=6'} @@ -4679,6 +4754,10 @@ packages: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -5038,6 +5117,10 @@ packages: hastscript@9.0.1: resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + hono@4.12.25: + resolution: {integrity: sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==} + engines: {node: '>=16.9.0'} + hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} @@ -5054,6 +5137,10 @@ packages: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + http-signature@1.2.0: resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==} engines: {node: '>=0.8', npm: '>=1.3.7'} @@ -5075,6 +5162,10 @@ packages: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + ieee754@1.1.13: resolution: {integrity: sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==} @@ -5124,6 +5215,10 @@ packages: resolution: {integrity: sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==} engines: {node: '>=12.22.0'} + ip-address@10.2.0: + resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} + engines: {node: '>= 12'} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -5281,6 +5376,9 @@ packages: resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} engines: {node: '>=0.10.0'} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -5532,8 +5630,8 @@ packages: joi@17.13.3: resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==} - jose@5.10.0: - resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} + jose@6.2.3: + resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} @@ -5583,6 +5681,9 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} @@ -5939,6 +6040,10 @@ packages: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + medialit@0.2.0: resolution: {integrity: sha512-wEScFuowUC8P6F8aLrcpsjm07GhjWMxnjMLdn5ley1PIrqxBcn7On0OqVcqf+c2VbM7CO9DHacLRGk4xZKgLwA==} engines: {node: '>=18.0.0'} @@ -5949,6 +6054,10 @@ packages: merge-descriptors@1.0.3: resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -6077,10 +6186,18 @@ packages: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} engines: {node: '>=4'} @@ -6119,33 +6236,6 @@ packages: mongodb-connection-string-url@3.0.2: resolution: {integrity: sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==} - mongodb@6.15.0: - resolution: {integrity: sha512-ifBhQ0rRzHDzqp9jAQP6OwHSH7dbYIQjD3SbJs9YYk9AikKEettW/9s/tbSFDTpXcRbF+u1aLrhHxDFaYtZpFQ==} - engines: {node: '>=16.20.1'} - peerDependencies: - '@aws-sdk/credential-providers': ^3.188.0 - '@mongodb-js/zstd': ^1.1.0 || ^2.0.0 - gcp-metadata: ^5.2.0 - kerberos: ^2.0.1 - mongodb-client-encryption: '>=6.0.0 <7' - snappy: ^7.2.2 - socks: ^2.7.1 - peerDependenciesMeta: - '@aws-sdk/credential-providers': - optional: true - '@mongodb-js/zstd': - optional: true - gcp-metadata: - optional: true - kerberos: - optional: true - mongodb-client-encryption: - optional: true - snappy: - optional: true - socks: - optional: true - mongodb@6.20.0: resolution: {integrity: sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==} engines: {node: '>=16.20.1'} @@ -6173,10 +6263,6 @@ packages: socks: optional: true - mongoose@8.13.2: - resolution: {integrity: sha512-riCBqZmNkYBWjXpM3qWLDQw7QmTKsVZDPhLXFJqC87+OjocEVpvS3dA2BPPUiLAu+m0/QmEj5pSXKhH+/DgerQ==} - engines: {node: '>=16.20.1'} - mongoose@8.19.3: resolution: {integrity: sha512-fTAGaIohkk8wCggMuBuqTVD4YrM1/J8cBr1ekqzFqtz65qkLjtX2dcy3NH1e+2rk2365dyrrsPAnt4YTxBhEiQ==} engines: {node: '>=16.20.1'} @@ -6253,22 +6339,6 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} - next-auth@5.0.0-beta.25: - resolution: {integrity: sha512-2dJJw1sHQl2qxCrRk+KTQbeH+izFbGFPuJj5eGgBZFYyiYYtvlrBeUw1E/OJJxTRjuxbSYGnCTkUIRsIIW0bog==} - peerDependencies: - '@simplewebauthn/browser': ^9.0.1 - '@simplewebauthn/server': ^9.0.2 - next: ^14.0.0-0 || ^15.0.0-0 - nodemailer: ^6.6.5 - react: ^18.2.0 || ^19.0.0-0 - peerDependenciesMeta: - '@simplewebauthn/browser': - optional: true - '@simplewebauthn/server': - optional: true - nodemailer: - optional: true - next-themes@0.2.1: resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==} peerDependencies: @@ -6379,9 +6449,6 @@ packages: oauth-sign@0.9.0: resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==} - oauth4webapi@3.4.0: - resolution: {integrity: sha512-5lcbectYuzQHvh0Ni7Epvc13sMVq7BxWUlHEYHaNko64OA1hcats0Huq30vZjqCZULcVE/PZxAGGPansfRAWKQ==} - object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -6671,6 +6738,10 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + pkg-dir@3.0.0: resolution: {integrity: sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==} engines: {node: '>=6'} @@ -6753,14 +6824,6 @@ packages: resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} engines: {node: ^10 || ^12 || >=14} - preact-render-to-string@5.2.3: - resolution: {integrity: sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==} - peerDependencies: - preact: '>=10' - - preact@10.11.3: - resolution: {integrity: sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==} - prelude-ls@1.1.2: resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} engines: {node: '>= 0.8.0'} @@ -6783,9 +6846,6 @@ packages: resolution: {integrity: sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==} engines: {node: '>= 6'} - pretty-format@3.8.0: - resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==} - process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} @@ -6829,6 +6889,10 @@ packages: resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} engines: {node: '>=0.6'} + qs@6.15.2: + resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} + engines: {node: '>=0.6'} + qs@6.5.3: resolution: {integrity: sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==} engines: {node: '>=0.6'} @@ -6858,6 +6922,10 @@ packages: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + react-dom@19.1.0: resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} peerDependencies: @@ -7154,6 +7222,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + rsvp@4.8.5: resolution: {integrity: sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==} engines: {node: 6.* || >= 7.*} @@ -7232,10 +7304,18 @@ packages: resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + serve-static@1.16.2: resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} engines: {node: '>= 0.8.0'} + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} @@ -7450,6 +7530,10 @@ packages: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + stealthy-require@1.1.1: resolution: {integrity: sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g==} engines: {node: '>=0.10.0'} @@ -7811,6 +7895,14 @@ packages: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + + type-is@2.1.0: + resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} + engines: {node: '>= 18'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -7962,11 +8054,12 @@ packages: uuid@3.4.0: resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} - deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true uuid@8.0.0: resolution: {integrity: sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true v8-compile-cache-lib@3.0.1: @@ -8011,6 +8104,7 @@ packages: whatwg-encoding@1.0.5: resolution: {integrity: sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-mimetype@2.3.0: resolution: {integrity: sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==} @@ -8142,9 +8236,17 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zod-to-json-schema@3.25.2: + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} + peerDependencies: + zod: ^3.25.28 || ^4 + zod@3.24.2: resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==} + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} @@ -8160,18 +8262,6 @@ snapshots: '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 - '@auth/core@0.37.2(nodemailer@6.10.0)': - dependencies: - '@panva/hkdf': 1.2.1 - '@types/cookie': 0.6.0 - cookie: 0.7.1 - jose: 5.10.0 - oauth4webapi: 3.4.0 - preact: 10.11.3 - preact-render-to-string: 5.2.3(preact@10.11.3) - optionalDependencies: - nodemailer: 6.10.0 - '@aws-crypto/crc32@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 @@ -8684,7 +8774,7 @@ snapshots: '@babel/traverse': 7.27.0 '@babel/types': 7.27.0 convert-source-map: 2.0.0 - debug: 4.4.0(supports-color@5.5.0) + debug: 4.4.3 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -8762,7 +8852,7 @@ snapshots: '@babel/parser': 7.27.0 '@babel/template': 7.27.0 '@babel/types': 7.27.0 - debug: 4.4.0(supports-color@5.5.0) + debug: 4.4.3 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -9107,7 +9197,7 @@ snapshots: '@eslint/config-array@0.20.0': dependencies: '@eslint/object-schema': 2.1.6 - debug: 4.4.0(supports-color@5.5.0) + debug: 4.4.3 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -9125,7 +9215,7 @@ snapshots: '@eslint/eslintrc@2.1.4': dependencies: ajv: 6.12.6 - debug: 4.4.0(supports-color@5.5.0) + debug: 4.4.3 espree: 9.6.1 globals: 13.24.0 ignore: 5.3.2 @@ -9192,9 +9282,11 @@ snapshots: optionalDependencies: tailwindcss: 4.1.4 - '@fumari/json-schema-ts@0.0.2': + '@fumari/json-schema-ts@0.0.2(json-schema-typed@8.0.2)': dependencies: esrap: 2.2.4 + optionalDependencies: + json-schema-typed: 8.0.2 '@fumari/stf@1.0.3(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: @@ -9209,6 +9301,10 @@ snapshots: dependencies: '@hapi/hoek': 9.3.0 + '@hono/node-server@1.19.14(hono@4.12.25)': + dependencies: + hono: 4.12.25 + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -9219,7 +9315,7 @@ snapshots: '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.4.0(supports-color@5.5.0) + debug: 4.4.3 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -9444,9 +9540,7 @@ snapshots: jest-runner: 24.9.0 jest-runtime: 24.9.0 transitivePeerDependencies: - - bufferutil - supports-color - - utf-8-validate '@jest/transform@24.9.0': dependencies: @@ -9543,9 +9637,27 @@ snapshots: transitivePeerDependencies: - supports-color - '@mongodb-js/saslprep@1.2.2': + '@modelcontextprotocol/sdk@1.29.0(zod@3.25.76)': dependencies: - sparse-bitfield: 3.0.3 + '@hono/node-server': 1.19.14(hono@4.12.25) + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + content-type: 1.0.5 + cors: 2.8.5 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.1.0 + express: 5.2.1 + express-rate-limit: 8.5.2(express@5.2.1) + hono: 4.12.25 + jose: 6.2.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 3.25.76 + zod-to-json-schema: 3.25.2(zod@3.25.76) + transitivePeerDependencies: + - supports-color '@mongodb-js/saslprep@1.3.2': dependencies: @@ -9618,6 +9730,14 @@ snapshots: '@next/swc-win32-x64-msvc@16.2.0': optional: true + '@node-oauth/formats@1.0.0': {} + + '@node-oauth/oauth2-server@5.3.0': + dependencies: + '@node-oauth/formats': 1.0.0 + basic-auth: 2.0.1 + type-is: 2.0.1 + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -9634,8 +9754,6 @@ snapshots: '@orama/orama@3.1.18': {} - '@panva/hkdf@1.2.1': {} - '@pinojs/redact@0.4.0': {} '@pkgjs/parseargs@0.11.0': @@ -11104,8 +11222,6 @@ snapshots: dependencies: '@types/node': 20.10.6 - '@types/cookie@0.6.0': {} - '@types/cors@2.8.17': dependencies: '@types/node': 20.10.6 @@ -11164,8 +11280,9 @@ snapshots: '@types/json5@0.0.29': {} - '@types/jsonwebtoken@8.5.9': + '@types/jsonwebtoken@9.0.10': dependencies: + '@types/ms': 2.1.0 '@types/node': 20.10.6 '@types/mdast@4.0.4': @@ -11176,19 +11293,6 @@ snapshots: '@types/mime@1.3.5': {} - '@types/mongoose@5.11.97': - dependencies: - mongoose: 8.19.3 - transitivePeerDependencies: - - '@aws-sdk/credential-providers' - - '@mongodb-js/zstd' - - gcp-metadata - - kerberos - - mongodb-client-encryption - - snappy - - socks - - supports-color - '@types/ms@2.1.0': {} '@types/node@12.20.55': {} @@ -11208,7 +11312,7 @@ snapshots: '@types/passport-jwt@3.0.13': dependencies: '@types/express': 4.17.21 - '@types/jsonwebtoken': 8.5.9 + '@types/jsonwebtoken': 9.0.10 '@types/passport-strategy': 0.2.38 '@types/passport-strategy@0.2.38': @@ -11478,7 +11582,7 @@ snapshots: fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 - semver: 7.7.1 + semver: 7.7.3 ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: @@ -11587,6 +11691,11 @@ snapshots: mime-types: 2.1.35 negotiator: 0.6.3 + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + acorn-globals@4.3.4: dependencies: acorn: 6.4.2 @@ -11610,6 +11719,10 @@ snapshots: acorn@8.14.1: {} + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -11881,6 +11994,10 @@ snapshots: baseline-browser-mapping@2.10.9: {} + basic-auth@2.0.1: + dependencies: + safe-buffer: 5.1.2 + bcrypt-pbkdf@1.0.2: dependencies: tweetnacl: 0.14.5 @@ -11913,6 +12030,20 @@ snapshots: transitivePeerDependencies: - supports-color + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.0 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.2 + raw-body: 3.0.2 + type-is: 2.1.0 + transitivePeerDependencies: + - supports-color + bowser@2.11.0: {} brace-expansion@1.1.11: @@ -11960,8 +12091,6 @@ snapshots: dependencies: node-int64: 0.4.0 - bson@6.10.3: {} - bson@6.10.4: {} buffer-equal-constant-time@1.0.1: {} @@ -12169,14 +12298,20 @@ snapshots: dependencies: safe-buffer: 5.2.1 + content-disposition@1.1.0: {} + content-type@1.0.5: {} + content-type@2.0.0: {} + convert-source-map@1.9.0: {} convert-source-map@2.0.0: {} cookie-signature@1.0.6: {} + cookie-signature@1.2.2: {} + cookie@0.7.1: {} copy-descriptor@0.1.1: {} @@ -12260,6 +12395,10 @@ snapshots: optionalDependencies: supports-color: 5.5.0 + debug@4.4.3: + dependencies: + ms: 2.1.3 + decamelize@1.2.0: {} decode-named-character-reference@1.1.0: @@ -12651,7 +12790,7 @@ snapshots: eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0)(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.0(supports-color@5.5.0) + debug: 4.4.3 eslint: 8.57.1 get-tsconfig: 4.10.0 is-bun-module: 2.0.0 @@ -12666,7 +12805,7 @@ snapshots: eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0)(eslint@9.24.0(jiti@2.4.2)): dependencies: '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.0(supports-color@5.5.0) + debug: 4.4.3 eslint: 9.24.0(jiti@2.4.2) get-tsconfig: 4.10.0 is-bun-module: 2.0.0 @@ -13028,6 +13167,12 @@ snapshots: events@1.1.1: {} + eventsource-parser@3.1.0: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.1.0 + exec-sh@0.3.6: {} execa@1.0.0: @@ -13081,6 +13226,15 @@ snapshots: dependencies: busboy: 1.6.0 + express-rate-limit@7.5.1(express@4.21.2): + dependencies: + express: 4.21.2 + + express-rate-limit@8.5.2(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.2.0 + express@4.21.2: dependencies: accepts: 1.3.8 @@ -13117,6 +13271,39 @@ snapshots: transitivePeerDependencies: - supports-color + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.0 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.2 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.1 + type-is: 2.1.0 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + extend-shallow@2.0.1: dependencies: is-extendable: 0.1.1 @@ -13239,6 +13426,17 @@ snapshots: transitivePeerDependencies: - supports-color + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + find-up@3.0.0: dependencies: locate-path: 3.0.0 @@ -13313,6 +13511,8 @@ snapshots: fresh@0.5.2: {} + fresh@2.0.0: {} + fs-extra@7.0.1: dependencies: graceful-fs: 4.2.11 @@ -13404,10 +13604,10 @@ snapshots: transitivePeerDependencies: - supports-color - fumadocs-openapi@10.4.1(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(fumadocs-core@16.7.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.2)(lucide-react@0.487.0(react@19.2.0))(next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(zod@4.3.6))(fumadocs-ui@16.7.1(@types/mdx@2.0.13)(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(fumadocs-core@16.7.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.2)(lucide-react@0.487.0(react@19.2.0))(next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(zod@4.3.6))(next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(shiki@4.0.2)(tailwindcss@4.1.4))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(shiki@4.0.2): + fumadocs-openapi@10.4.1(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(fumadocs-core@16.7.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.2)(lucide-react@0.487.0(react@19.2.0))(next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(zod@4.3.6))(fumadocs-ui@16.7.1(@types/mdx@2.0.13)(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(fumadocs-core@16.7.1(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.2)(lucide-react@0.487.0(react@19.2.0))(next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(zod@4.3.6))(next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(shiki@4.0.2)(tailwindcss@4.1.4))(json-schema-typed@8.0.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(shiki@4.0.2): dependencies: '@fastify/deepmerge': 3.2.1 - '@fumari/json-schema-ts': 0.0.2 + '@fumari/json-schema-ts': 0.0.2(json-schema-typed@8.0.2) '@fumari/stf': 1.0.3(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@radix-ui/react-accordion': 1.2.12(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -13435,6 +13635,7 @@ snapshots: xml-js: 1.6.11 optionalDependencies: '@types/react': 19.2.2 + json-schema-typed: 8.0.2 shiki: 4.0.2 transitivePeerDependencies: - '@types/react-dom' @@ -13762,6 +13963,8 @@ snapshots: property-information: 7.0.0 space-separated-tokens: 2.0.2 + hono@4.12.25: {} + hosted-git-info@2.8.9: {} html-encoding-sniffer@1.0.2: @@ -13780,6 +13983,14 @@ snapshots: statuses: 2.0.1 toidentifier: 1.0.1 + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + http-signature@1.2.0: dependencies: assert-plus: 1.0.0 @@ -13796,6 +14007,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + ieee754@1.1.13: {} ignore-by-default@1.0.1: {} @@ -13850,6 +14065,8 @@ snapshots: - supports-color optional: true + ip-address@10.2.0: {} + ipaddr.js@1.9.1: {} is-accessor-descriptor@1.0.1: @@ -13998,6 +14215,8 @@ snapshots: dependencies: isobject: 3.0.1 + is-promise@4.0.0: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -14089,7 +14308,7 @@ snapshots: istanbul-lib-source-maps@3.0.6: dependencies: - debug: 4.4.0(supports-color@5.5.0) + debug: 4.4.3 istanbul-lib-coverage: 2.0.5 make-dir: 2.1.0 rimraf: 2.7.1 @@ -14451,7 +14670,7 @@ snapshots: '@sideway/formula': 3.0.1 '@sideway/pinpoint': 2.0.0 - jose@5.10.0: {} + jose@6.2.3: {} joycon@3.1.1: {} @@ -14520,6 +14739,8 @@ snapshots: json-schema-traverse@1.0.0: {} + json-schema-typed@8.0.2: {} + json-schema@0.4.0: {} json-stable-stringify-without-jsonify@1.0.1: {} @@ -14547,7 +14768,7 @@ snapshots: lodash.isstring: 4.0.1 lodash.once: 4.1.1 ms: 2.1.3 - semver: 7.7.1 + semver: 7.7.3 jsprim@1.4.2: dependencies: @@ -14981,6 +15202,8 @@ snapshots: media-typer@0.3.0: {} + media-typer@1.1.0: {} + medialit@0.2.0: dependencies: form-data: 4.0.2 @@ -14989,6 +15212,8 @@ snapshots: merge-descriptors@1.0.3: {} + merge-descriptors@2.0.0: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -15240,7 +15465,7 @@ snapshots: micromark@4.0.2: dependencies: '@types/debug': 4.1.12 - debug: 4.4.0(supports-color@5.5.0) + debug: 4.4.3 decode-named-character-reference: 1.1.0 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 @@ -15284,10 +15509,16 @@ snapshots: mime-db@1.52.0: {} + mime-db@1.54.0: {} + mime-types@2.1.35: dependencies: mime-db: 1.52.0 + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + mime@1.6.0: {} mimic-fn@4.0.0: {} @@ -15320,37 +15551,12 @@ snapshots: '@types/whatwg-url': 11.0.5 whatwg-url: 14.2.0 - mongodb@6.15.0: - dependencies: - '@mongodb-js/saslprep': 1.2.2 - bson: 6.10.3 - mongodb-connection-string-url: 3.0.2 - mongodb@6.20.0: dependencies: '@mongodb-js/saslprep': 1.3.2 bson: 6.10.4 mongodb-connection-string-url: 3.0.2 - mongoose@8.13.2: - dependencies: - bson: 6.10.3 - kareem: 2.6.3 - mongodb: 6.15.0 - mpath: 0.9.0 - mquery: 5.0.0 - ms: 2.1.3 - sift: 17.1.3 - transitivePeerDependencies: - - '@aws-sdk/credential-providers' - - '@mongodb-js/zstd' - - gcp-metadata - - kerberos - - mongodb-client-encryption - - snappy - - socks - - supports-color - mongoose@8.19.3: dependencies: bson: 6.10.4 @@ -15388,7 +15594,7 @@ snapshots: mquery@5.0.0: dependencies: - debug: 4.4.0(supports-color@5.5.0) + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -15443,14 +15649,6 @@ snapshots: negotiator@1.0.0: {} - next-auth@5.0.0-beta.25(next@15.5.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(nodemailer@6.10.0)(react@19.2.0): - dependencies: - '@auth/core': 0.37.2(nodemailer@6.10.0) - next: 15.5.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - react: 19.2.0 - optionalDependencies: - nodemailer: 6.10.0 - next-themes@0.2.1(next@15.5.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: next: 15.5.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -15590,8 +15788,6 @@ snapshots: oauth-sign@0.9.0: {} - oauth4webapi@3.4.0: {} - object-assign@4.1.1: {} object-copy@0.1.0: @@ -15879,6 +16075,8 @@ snapshots: pirates@4.0.7: {} + pkce-challenge@5.0.1: {} + pkg-dir@3.0.0: dependencies: find-up: 3.0.0 @@ -15947,13 +16145,6 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - preact-render-to-string@5.2.3(preact@10.11.3): - dependencies: - preact: 10.11.3 - pretty-format: 3.8.0 - - preact@10.11.3: {} - prelude-ls@1.1.2: {} prelude-ls@1.2.1: {} @@ -15969,8 +16160,6 @@ snapshots: ansi-styles: 3.2.1 react-is: 16.13.1 - pretty-format@3.8.0: {} - process-nextick-args@2.0.1: {} process-warning@5.0.0: {} @@ -16018,6 +16207,10 @@ snapshots: dependencies: side-channel: 1.1.0 + qs@6.15.2: + dependencies: + side-channel: 1.1.0 + qs@6.5.3: {} quansync@0.2.10: {} @@ -16039,6 +16232,13 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + react-dom@19.1.0(react@19.1.0): dependencies: react: 19.1.0 @@ -16415,6 +16615,16 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.40.0 fsevents: 2.3.3 + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + rsvp@4.8.5: {} run-parallel@1.2.0: @@ -16504,6 +16714,22 @@ snapshots: transitivePeerDependencies: - supports-color + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + serve-static@1.16.2: dependencies: encodeurl: 2.0.0 @@ -16513,6 +16739,15 @@ snapshots: transitivePeerDependencies: - supports-color + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + set-blocking@2.0.0: {} set-cookie-parser@2.7.1: {} @@ -16781,6 +17016,8 @@ snapshots: statuses@2.0.1: {} + statuses@2.0.2: {} + stealthy-require@1.1.1: {} streamsearch@1.1.0: {} @@ -17226,6 +17463,18 @@ snapshots: media-typer: 0.3.0 mime-types: 2.1.35 + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + + type-is@2.1.0: + dependencies: + content-type: 2.0.0 + media-typer: 1.1.0 + mime-types: 3.0.2 + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -17638,8 +17887,14 @@ snapshots: yocto-queue@0.1.0: {} + zod-to-json-schema@3.25.2(zod@3.25.76): + dependencies: + zod: 3.25.76 + zod@3.24.2: {} + zod@3.25.76: {} + zod@4.3.6: {} zwitch@2.0.4: {}